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.
Files changed (82) hide show
  1. package/README.md +65 -21
  2. package/dist/cjs/cli/config.d.ts +40 -0
  3. package/dist/cjs/cli/config.js +74 -2
  4. package/dist/cjs/cli/index.d.ts +85 -1
  5. package/dist/cjs/cli/index.js +323 -24
  6. package/dist/cjs/cli/mcp.d.ts +8 -0
  7. package/dist/cjs/cli/mcp.js +448 -29
  8. package/dist/cjs/cli/pii-tags.d.ts +64 -9
  9. package/dist/cjs/cli/pii-tags.js +218 -39
  10. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  11. package/dist/cjs/cli/studio.d.ts +23 -0
  12. package/dist/cjs/cli/studio.js +126 -53
  13. package/dist/cjs/cli/ui.d.ts +15 -1
  14. package/dist/cjs/cli/ui.js +19 -5
  15. package/dist/cjs/client.js +186 -3
  16. package/dist/cjs/errors.d.ts +38 -1
  17. package/dist/cjs/errors.js +235 -24
  18. package/dist/cjs/index.d.ts +2 -2
  19. package/dist/cjs/index.js +7 -2
  20. package/dist/cjs/pipeline.js +15 -2
  21. package/dist/cjs/powql.d.ts +12 -0
  22. package/dist/cjs/powql.js +46 -21
  23. package/dist/cjs/prisma-compat.d.ts +15 -5
  24. package/dist/cjs/prisma-compat.js +273 -78
  25. package/dist/cjs/query/aggregates.d.ts +1 -1
  26. package/dist/cjs/query/aggregates.js +24 -10
  27. package/dist/cjs/query/batched-loader.d.ts +9 -4
  28. package/dist/cjs/query/batched-loader.js +4 -1
  29. package/dist/cjs/query/builder.d.ts +47 -0
  30. package/dist/cjs/query/builder.js +125 -21
  31. package/dist/cjs/query/index.d.ts +3 -1
  32. package/dist/cjs/query/index.js +7 -1
  33. package/dist/cjs/query/option-surface.d.ts +11 -0
  34. package/dist/cjs/query/option-surface.js +13 -0
  35. package/dist/cjs/query/relations.d.ts +8 -0
  36. package/dist/cjs/query/relations.js +21 -1
  37. package/dist/cjs/query/types.d.ts +152 -18
  38. package/dist/cjs/query/types.js +212 -1
  39. package/dist/cjs/query/where.d.ts +3 -3
  40. package/dist/cjs/query/where.js +8 -2
  41. package/dist/cjs/query/writes.js +10 -9
  42. package/dist/cli/config.d.ts +40 -0
  43. package/dist/cli/config.js +73 -2
  44. package/dist/cli/index.d.ts +85 -1
  45. package/dist/cli/index.js +321 -26
  46. package/dist/cli/mcp.d.ts +8 -0
  47. package/dist/cli/mcp.js +448 -29
  48. package/dist/cli/pii-tags.d.ts +64 -9
  49. package/dist/cli/pii-tags.js +217 -39
  50. package/dist/cli/studio-ui.generated.js +1 -1
  51. package/dist/cli/studio.d.ts +23 -0
  52. package/dist/cli/studio.js +125 -53
  53. package/dist/cli/ui.d.ts +15 -1
  54. package/dist/cli/ui.js +18 -4
  55. package/dist/client.js +187 -4
  56. package/dist/errors.d.ts +38 -1
  57. package/dist/errors.js +234 -23
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +5 -2
  60. package/dist/pipeline.js +15 -2
  61. package/dist/powql.d.ts +12 -0
  62. package/dist/powql.js +46 -21
  63. package/dist/prisma-compat.d.ts +15 -5
  64. package/dist/prisma-compat.js +274 -79
  65. package/dist/query/aggregates.d.ts +1 -1
  66. package/dist/query/aggregates.js +24 -10
  67. package/dist/query/batched-loader.d.ts +9 -4
  68. package/dist/query/batched-loader.js +4 -1
  69. package/dist/query/builder.d.ts +47 -0
  70. package/dist/query/builder.js +124 -21
  71. package/dist/query/index.d.ts +3 -1
  72. package/dist/query/index.js +2 -0
  73. package/dist/query/option-surface.d.ts +11 -0
  74. package/dist/query/option-surface.js +13 -0
  75. package/dist/query/relations.d.ts +8 -0
  76. package/dist/query/relations.js +21 -1
  77. package/dist/query/types.d.ts +152 -18
  78. package/dist/query/types.js +207 -2
  79. package/dist/query/where.d.ts +3 -3
  80. package/dist/query/where.js +8 -2
  81. package/dist/query/writes.js +10 -9
  82. package/package.json +13 -3
@@ -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 _url = connectionString ?? 'process.env.DATABASE_URL';
195
- const urlLine = connectionString ? ` url: '${connectionString}',` : ` url: process.env.DATABASE_URL,`;
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
  /**
@@ -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;