turbine-orm 0.49.0 → 0.50.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 (157) hide show
  1. package/README.md +122 -39
  2. package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
  3. package/dist/cjs/adapters/index.d.ts +110 -0
  4. package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
  5. package/dist/cjs/cli/config.d.ts +181 -0
  6. package/dist/cjs/cli/config.js +32 -6
  7. package/dist/cjs/cli/destructive.d.ts +38 -0
  8. package/dist/cjs/cli/index.d.ts +359 -0
  9. package/dist/cjs/cli/index.js +228 -56
  10. package/dist/cjs/cli/loader.d.ts +61 -0
  11. package/dist/cjs/cli/mcp.d.ts +42 -0
  12. package/dist/cjs/cli/migrate.d.ts +356 -0
  13. package/dist/cjs/cli/migrate.js +131 -40
  14. package/dist/cjs/cli/observe-ui.d.ts +1 -0
  15. package/dist/cjs/cli/observe-ui.js +14 -5
  16. package/dist/cjs/cli/observe.d.ts +25 -0
  17. package/dist/cjs/cli/observe.js +49 -12
  18. package/dist/cjs/cli/pii-tags.d.ts +53 -0
  19. package/dist/cjs/cli/prisma-report.d.ts +33 -0
  20. package/dist/cjs/cli/prisma-report.js +73 -0
  21. package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
  22. package/dist/cjs/cli/prisma-resolve.js +1 -0
  23. package/dist/cjs/cli/prisma-schema.d.ts +176 -0
  24. package/dist/cjs/cli/prisma-schema.js +82 -4
  25. package/dist/cjs/cli/rate-limit.d.ts +32 -0
  26. package/dist/cjs/cli/rate-limit.js +45 -0
  27. package/dist/cjs/cli/studio-demo.d.ts +43 -0
  28. package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
  29. package/dist/cjs/cli/studio.d.ts +207 -0
  30. package/dist/cjs/cli/studio.js +136 -71
  31. package/dist/cjs/cli/ui.d.ts +73 -0
  32. package/dist/cjs/cli/ui.js +51 -9
  33. package/dist/cjs/client.d.ts +837 -0
  34. package/dist/cjs/client.js +3 -0
  35. package/dist/cjs/dialect.d.ts +516 -0
  36. package/dist/cjs/dialect.js +37 -12
  37. package/dist/cjs/errors.d.ts +370 -0
  38. package/dist/cjs/generate.d.ts +137 -0
  39. package/dist/cjs/generate.js +39 -6
  40. package/dist/cjs/index-advisor.d.ts +153 -0
  41. package/dist/cjs/index-stats.d.ts +384 -0
  42. package/dist/cjs/index.d.ts +55 -0
  43. package/dist/cjs/index.js +7 -2
  44. package/dist/cjs/introspect.d.ts +269 -0
  45. package/dist/cjs/mssql.d.ts +232 -0
  46. package/dist/cjs/mssql.js +6 -0
  47. package/dist/cjs/mysql.d.ts +173 -0
  48. package/dist/cjs/mysql.js +16 -0
  49. package/dist/cjs/nested-write.d.ts +96 -0
  50. package/dist/cjs/nested-write.js +414 -24
  51. package/dist/cjs/observe.d.ts +115 -0
  52. package/dist/cjs/optional-peer-import.d.cts +72 -0
  53. package/dist/cjs/pipeline-submittable.d.ts +93 -0
  54. package/dist/cjs/pipeline.d.ts +71 -0
  55. package/dist/cjs/powdb-introspect.d.ts +84 -0
  56. package/dist/cjs/powdb.d.ts +931 -0
  57. package/dist/cjs/powdb.js +106 -21
  58. package/dist/cjs/powql.d.ts +592 -0
  59. package/dist/cjs/powql.js +42 -6
  60. package/dist/cjs/prisma-compat.d.ts +283 -0
  61. package/dist/cjs/prisma-compat.js +167 -9
  62. package/dist/cjs/query/aggregates.d.ts +92 -0
  63. package/dist/cjs/query/aggregates.js +7 -3
  64. package/dist/cjs/query/batched-loader.d.ts +193 -0
  65. package/dist/cjs/query/builder.d.ts +849 -0
  66. package/dist/cjs/query/builder.js +571 -65
  67. package/dist/cjs/query/compound-unique.d.ts +51 -0
  68. package/dist/cjs/query/deferred.d.ts +223 -0
  69. package/dist/cjs/query/filters.d.ts +201 -0
  70. package/dist/cjs/query/index.d.ts +14 -0
  71. package/dist/cjs/query/index.js +6 -1
  72. package/dist/cjs/query/relations.d.ts +609 -0
  73. package/dist/cjs/query/relations.js +693 -46
  74. package/dist/cjs/query/types.d.ts +1300 -0
  75. package/dist/cjs/query/utils.d.ts +209 -0
  76. package/dist/cjs/query/utils.js +208 -1
  77. package/dist/cjs/query/warn-registry.d.ts +68 -0
  78. package/dist/cjs/query/warn-registry.js +9 -0
  79. package/dist/cjs/query/where-compile.d.ts +139 -0
  80. package/dist/cjs/query/where.d.ts +548 -0
  81. package/dist/cjs/query/where.js +58 -22
  82. package/dist/cjs/query/writes.d.ts +172 -0
  83. package/dist/cjs/query/writes.js +105 -12
  84. package/dist/cjs/realtime.d.ts +70 -0
  85. package/dist/cjs/schema-builder.d.ts +354 -0
  86. package/dist/cjs/schema-metadata.d.ts +83 -0
  87. package/dist/cjs/schema-sql.d.ts +217 -0
  88. package/dist/cjs/schema-sql.js +23 -5
  89. package/dist/cjs/schema.d.ts +356 -0
  90. package/dist/cjs/schema.js +125 -0
  91. package/dist/cjs/seed.d.ts +15 -0
  92. package/dist/cjs/serverless.d.ts +142 -0
  93. package/dist/cjs/sqlite.d.ts +143 -0
  94. package/dist/cjs/sqlite.js +4 -0
  95. package/dist/cjs/typed-sql.d.ts +102 -0
  96. package/dist/cli/config.d.ts +18 -4
  97. package/dist/cli/config.js +31 -6
  98. package/dist/cli/index.d.ts +123 -0
  99. package/dist/cli/index.js +223 -58
  100. package/dist/cli/migrate.d.ts +59 -10
  101. package/dist/cli/migrate.js +128 -41
  102. package/dist/cli/observe-ui.d.ts +1 -1
  103. package/dist/cli/observe-ui.js +14 -5
  104. package/dist/cli/observe.d.ts +7 -1
  105. package/dist/cli/observe.js +48 -12
  106. package/dist/cli/prisma-report.d.ts +14 -0
  107. package/dist/cli/prisma-report.js +72 -0
  108. package/dist/cli/prisma-resolve.d.ts +6 -0
  109. package/dist/cli/prisma-resolve.js +1 -0
  110. package/dist/cli/prisma-schema.d.ts +62 -2
  111. package/dist/cli/prisma-schema.js +81 -4
  112. package/dist/cli/rate-limit.d.ts +32 -0
  113. package/dist/cli/rate-limit.js +40 -0
  114. package/dist/cli/studio.d.ts +5 -5
  115. package/dist/cli/studio.js +135 -70
  116. package/dist/cli/ui.d.ts +1 -1
  117. package/dist/cli/ui.js +51 -9
  118. package/dist/client.d.ts +40 -0
  119. package/dist/client.js +3 -0
  120. package/dist/dialect.d.ts +17 -1
  121. package/dist/dialect.js +37 -12
  122. package/dist/generate.js +40 -7
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/mssql.js +6 -0
  126. package/dist/mysql.js +16 -0
  127. package/dist/nested-write.d.ts +2 -0
  128. package/dist/nested-write.js +415 -25
  129. package/dist/powdb.d.ts +4 -2
  130. package/dist/powdb.js +106 -21
  131. package/dist/powql.d.ts +5 -0
  132. package/dist/powql.js +42 -6
  133. package/dist/prisma-compat.d.ts +2 -0
  134. package/dist/prisma-compat.js +166 -8
  135. package/dist/query/aggregates.js +7 -3
  136. package/dist/query/builder.d.ts +292 -21
  137. package/dist/query/builder.js +570 -64
  138. package/dist/query/deferred.d.ts +39 -0
  139. package/dist/query/index.d.ts +1 -1
  140. package/dist/query/index.js +1 -1
  141. package/dist/query/relations.d.ts +173 -5
  142. package/dist/query/relations.js +688 -47
  143. package/dist/query/types.d.ts +123 -39
  144. package/dist/query/utils.d.ts +116 -0
  145. package/dist/query/utils.js +198 -0
  146. package/dist/query/warn-registry.d.ts +9 -0
  147. package/dist/query/warn-registry.js +9 -0
  148. package/dist/query/where.d.ts +38 -1
  149. package/dist/query/where.js +58 -23
  150. package/dist/query/writes.d.ts +42 -1
  151. package/dist/query/writes.js +104 -13
  152. package/dist/schema-sql.d.ts +14 -0
  153. package/dist/schema-sql.js +23 -5
  154. package/dist/schema.d.ts +38 -0
  155. package/dist/schema.js +123 -0
  156. package/dist/sqlite.js +4 -0
  157. package/package.json +77 -28
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Hand-rolled `schema.prisma` subset parser (zero dependencies).
3
+ *
4
+ * Powers `turbine migrate-from-prisma`. It parses ONLY the constructs the
5
+ * name-mapper needs (models, enums, views, fields, `@map`/`@@map`, relations
6
+ * including implicit m2m junctions, `@@unique` including named selectors, and
7
+ * `@@id`) and is deliberately LENIENT everywhere else: any attribute, block, or
8
+ * token it does not recognize is skipped and recorded as a warning, never a
9
+ * fatal error. The live DATABASE is the authority for resolution, so a partial
10
+ * parse is still useful.
11
+ *
12
+ * It is a pure leaf like `cli/destructive.ts` - it reads a string and returns
13
+ * data, touches no filesystem, database, or process state, and imports nothing
14
+ * from the rest of the package.
15
+ *
16
+ * Where it MUST understand a construct (an unterminated block/string, a broken
17
+ * `@@id`/`@@unique`/`@@map`/`@relation`) it throws {@link PrismaParseError} with
18
+ * a 1-based line number.
19
+ */
20
+ /** A parsed attribute argument: positional or `key: value`, string/array/raw. */
21
+ export interface PrismaAttrArg {
22
+ /** Named-argument key (e.g. `fields`, `references`, `name`, `map`). Absent for positional args. */
23
+ key?: string;
24
+ /** `'string'` (unquoted literal), `'array'` (list of idents/strings), or `'raw'` (bare token). */
25
+ kind: 'string' | 'array' | 'raw';
26
+ /** Scalar value for string/raw kinds. */
27
+ value?: string;
28
+ /** Element list for the array kind (strings unquoted). */
29
+ items?: string[];
30
+ }
31
+ /** A field- or block-level attribute (`@map(...)`, `@@unique(...)`, ...). */
32
+ export interface PrismaAttr {
33
+ /** Attribute name without the leading `@`/`@@` (e.g. `map`, `relation`, `id`, `unique`). */
34
+ name: string;
35
+ /** Parsed argument list (empty when the attribute took no parens). */
36
+ args: PrismaAttrArg[];
37
+ /** True for a block attribute (`@@name`), false for a field attribute (`@name`). */
38
+ block: boolean;
39
+ /** 1-based source line the attribute was found on. */
40
+ line: number;
41
+ }
42
+ /** A parsed field line inside a model/view/type block. */
43
+ export interface PrismaField {
44
+ /** Field name as declared (the Prisma API name). */
45
+ name: string;
46
+ /** Base type with `[]` / `?` stripped (a scalar, enum, or model name). */
47
+ type: string;
48
+ /** Trailing `?` - the field is optional/nullable. */
49
+ optional: boolean;
50
+ /** Trailing `[]` - the field is a list. */
51
+ isList: boolean;
52
+ /** Field attributes in source order. */
53
+ attrs: PrismaAttr[];
54
+ /** 1-based source line. */
55
+ line: number;
56
+ }
57
+ /** A compound key derived from a `@@id` / `@@unique` block attribute. */
58
+ export interface PrismaCompoundKey {
59
+ /** Prisma field names participating, in declared order. */
60
+ fields: string[];
61
+ /** Explicit `name:` selector, else undefined (caller derives the underscore-join). */
62
+ name?: string;
63
+ /** Explicit `map:` constraint name, if any. */
64
+ map?: string;
65
+ /** `'id'` (from `@@id`) or `'unique'` (from `@@unique`). */
66
+ kind: 'id' | 'unique';
67
+ /** 1-based source line of the block attribute. */
68
+ line: number;
69
+ }
70
+ /** A parsed `model` / `view` / `type` block. */
71
+ export interface PrismaModel {
72
+ name: string;
73
+ kind: 'model' | 'view' | 'type';
74
+ /** `@@map("...")` target table name, if present. */
75
+ map?: string;
76
+ fields: PrismaField[];
77
+ /** Compound keys from `@@id` and `@@unique`. */
78
+ compoundKeys: PrismaCompoundKey[];
79
+ /** Every block attribute (`@@index`, `@@schema`, ...), recorded verbatim. */
80
+ blockAttrs: PrismaAttr[];
81
+ /** 1-based source line of the block header. */
82
+ line: number;
83
+ }
84
+ /** A parsed `enum` block. */
85
+ export interface PrismaEnum {
86
+ name: string;
87
+ /** Value names in declared order. */
88
+ values: string[];
89
+ /** `@@map("...")` target enum-type name, if present. */
90
+ map?: string;
91
+ line: number;
92
+ }
93
+ /**
94
+ * A connection-string value inside a `datasource` block: either a literal
95
+ * string (`url = "postgres://..."`) or an `env("NAME")` indirection.
96
+ */
97
+ export type PrismaConnectionValue = {
98
+ kind: 'literal';
99
+ value: string;
100
+ } | {
101
+ kind: 'env';
102
+ variable: string;
103
+ };
104
+ /** A parsed `datasource` block. Only the connection keys are retained. */
105
+ export interface PrismaDatasource {
106
+ /** Block name (`datasource db { ... }` -> `db`). */
107
+ name: string;
108
+ /** `provider = "postgresql"`, when declared as a literal. */
109
+ provider?: string;
110
+ /** `url = ...`, when declared as a literal or an `env(...)` call. */
111
+ url?: PrismaConnectionValue;
112
+ /** `directUrl = ...` (Prisma's non-pooled connection), same forms as `url`. */
113
+ directUrl?: PrismaConnectionValue;
114
+ /** 1-based source line of the block header. */
115
+ line: number;
116
+ }
117
+ /** The full parse result. */
118
+ export interface PrismaSchemaAst {
119
+ models: PrismaModel[];
120
+ enums: PrismaEnum[];
121
+ /** Parsed `datasource` blocks, in source order. */
122
+ datasources: PrismaDatasource[];
123
+ /** Non-fatal notes: skipped/unknown blocks and attributes. */
124
+ warnings: string[];
125
+ }
126
+ /** Thrown for a malformed construct the parser must understand. Carries a line number. */
127
+ export declare class PrismaParseError extends Error {
128
+ readonly line: number;
129
+ constructor(message: string, line: number);
130
+ }
131
+ /** Where a datasource connection string was resolved from. */
132
+ export interface ResolvedPrismaDatasourceUrl {
133
+ /** The connection string itself. */
134
+ url: string;
135
+ /** Datasource block name it came from. */
136
+ datasource: string;
137
+ /** Which key supplied it. */
138
+ key: 'url' | 'directUrl';
139
+ /** The environment variable read, when the value was an `env(...)` call. */
140
+ variable?: string;
141
+ }
142
+ /** Result of {@link resolvePrismaDatasourceUrl}. */
143
+ export interface PrismaDatasourceUrlLookup {
144
+ /** The first usable connection string found, if any. */
145
+ resolved?: ResolvedPrismaDatasourceUrl;
146
+ /**
147
+ * Environment variable names the schema declares via `env(...)` that were
148
+ * unset or empty, in lookup order. Surfaced in the no-URL error so the user
149
+ * sees exactly which variable the schema asked for.
150
+ */
151
+ missingVariables: string[];
152
+ }
153
+ /**
154
+ * Resolve the connection string a `schema.prisma` declares, reading `env(...)`
155
+ * indirections out of the supplied environment (pure: the environment is an
156
+ * argument, never `process.env` directly).
157
+ *
158
+ * Per datasource block, `url` is preferred and `directUrl` is the fallback:
159
+ * `url` is what Prisma itself uses for everything but migrations, and a pooled
160
+ * `url` introspects the same catalog as its direct twin.
161
+ */
162
+ export declare function resolvePrismaDatasourceUrl(ast: Pick<PrismaSchemaAst, 'datasources'>, env: Record<string, string | undefined>): PrismaDatasourceUrlLookup;
163
+ /**
164
+ * Parse a `schema.prisma` source string into a {@link PrismaSchemaAst}.
165
+ *
166
+ * Understands: model / view / type / enum blocks; field lines with `@map`,
167
+ * `@id`, `@unique`, `@default`, `@updatedAt`, `@ignore`, `@relation`; and block
168
+ * attributes `@@map`, `@@id`, `@@unique`, `@@index`, `@@schema`; plus the
169
+ * `provider` / `url` / `directUrl` keys of each `datasource` block (see
170
+ * {@link resolvePrismaDatasourceUrl}). Unknown attributes and blocks are
171
+ * skipped into {@link PrismaSchemaAst.warnings}.
172
+ *
173
+ * @throws {@link PrismaParseError} on an unterminated block/paren/string or a
174
+ * structurally broken `@@id` / `@@unique` / `@@map`.
175
+ */
176
+ export declare function parsePrismaSchema(source: string): PrismaSchemaAst;
@@ -20,6 +20,7 @@
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.PrismaParseError = void 0;
23
+ exports.resolvePrismaDatasourceUrl = resolvePrismaDatasourceUrl;
23
24
  exports.parsePrismaSchema = parsePrismaSchema;
24
25
  // ---------------------------------------------------------------------------
25
26
  // Error
@@ -437,6 +438,78 @@ function parseEnumBody(block, src) {
437
438
  }
438
439
  return en;
439
440
  }
441
+ /**
442
+ * Parse a `datasource` block value: a quoted literal, or an `env("NAME")` call.
443
+ * Returns null for any other form (e.g. an interpolated expression), which the
444
+ * caller records as "declared but not usable" rather than guessing.
445
+ */
446
+ function parseConnectionValue(raw) {
447
+ if (/^"(?:[^"\\]|\\.)*"$/.test(raw))
448
+ return { kind: 'literal', value: unquote(raw) };
449
+ const envCall = raw.match(/^env\(\s*("(?:[^"\\]|\\.)*")\s*\)$/);
450
+ if (envCall) {
451
+ const variable = unquote(envCall[1]);
452
+ return variable ? { kind: 'env', variable } : null;
453
+ }
454
+ return null;
455
+ }
456
+ /**
457
+ * Parse a `datasource` block. Only `provider`, `url`, and `directUrl` are kept;
458
+ * every other key (`shadowDatabaseUrl`, `relationMode`, `extensions`, ...) is
459
+ * irrelevant to name mapping and skipped silently, as before.
460
+ */
461
+ function parseDatasourceBody(block, src) {
462
+ const ds = { name: block.name, line: block.headerLine };
463
+ for (const { text } of bodyLines(block.body, block.bodyOffset, src)) {
464
+ const m = text.match(/^([A-Za-z_]\w*)\s*=\s*(.+)$/);
465
+ if (!m)
466
+ continue;
467
+ const key = m[1];
468
+ const raw = m[2].trim();
469
+ if (key === 'provider') {
470
+ const value = parseConnectionValue(raw);
471
+ if (value?.kind === 'literal')
472
+ ds.provider = value.value;
473
+ }
474
+ else if (key === 'url' || key === 'directUrl') {
475
+ const value = parseConnectionValue(raw);
476
+ if (value)
477
+ ds[key] = value;
478
+ }
479
+ }
480
+ return ds;
481
+ }
482
+ /**
483
+ * Resolve the connection string a `schema.prisma` declares, reading `env(...)`
484
+ * indirections out of the supplied environment (pure: the environment is an
485
+ * argument, never `process.env` directly).
486
+ *
487
+ * Per datasource block, `url` is preferred and `directUrl` is the fallback:
488
+ * `url` is what Prisma itself uses for everything but migrations, and a pooled
489
+ * `url` introspects the same catalog as its direct twin.
490
+ */
491
+ function resolvePrismaDatasourceUrl(ast, env) {
492
+ const missingVariables = [];
493
+ for (const ds of ast.datasources) {
494
+ for (const key of ['url', 'directUrl']) {
495
+ const value = ds[key];
496
+ if (!value)
497
+ continue;
498
+ if (value.kind === 'literal') {
499
+ if (value.value)
500
+ return { resolved: { url: value.value, datasource: ds.name, key }, missingVariables };
501
+ continue;
502
+ }
503
+ const fromEnv = env[value.variable];
504
+ if (fromEnv) {
505
+ return { resolved: { url: fromEnv, datasource: ds.name, key, variable: value.variable }, missingVariables };
506
+ }
507
+ if (!missingVariables.includes(value.variable))
508
+ missingVariables.push(value.variable);
509
+ }
510
+ }
511
+ return { missingVariables };
512
+ }
440
513
  // ---------------------------------------------------------------------------
441
514
  // Entry point
442
515
  // ---------------------------------------------------------------------------
@@ -445,15 +518,17 @@ function parseEnumBody(block, src) {
445
518
  *
446
519
  * Understands: model / view / type / enum blocks; field lines with `@map`,
447
520
  * `@id`, `@unique`, `@default`, `@updatedAt`, `@ignore`, `@relation`; and block
448
- * attributes `@@map`, `@@id`, `@@unique`, `@@index`, `@@schema`. Unknown
449
- * attributes and blocks are skipped into {@link PrismaSchemaAst.warnings}.
521
+ * attributes `@@map`, `@@id`, `@@unique`, `@@index`, `@@schema`; plus the
522
+ * `provider` / `url` / `directUrl` keys of each `datasource` block (see
523
+ * {@link resolvePrismaDatasourceUrl}). Unknown attributes and blocks are
524
+ * skipped into {@link PrismaSchemaAst.warnings}.
450
525
  *
451
526
  * @throws {@link PrismaParseError} on an unterminated block/paren/string or a
452
527
  * structurally broken `@@id` / `@@unique` / `@@map`.
453
528
  */
454
529
  function parsePrismaSchema(source) {
455
530
  const src = stripComments(source);
456
- const ast = { models: [], enums: [], warnings: [] };
531
+ const ast = { models: [], enums: [], datasources: [], warnings: [] };
457
532
  for (const block of scanBlocks(src)) {
458
533
  switch (block.keyword) {
459
534
  case 'model':
@@ -472,8 +547,11 @@ function parsePrismaSchema(source) {
472
547
  ast.enums.push(parseEnumBody(block, src));
473
548
  break;
474
549
  case 'datasource':
550
+ // Not a table, but it declares the connection string the CLI can reuse.
551
+ ast.datasources.push(parseDatasourceBody(block, src));
552
+ break;
475
553
  case 'generator':
476
- // Configuration blocks - irrelevant to name mapping.
554
+ // Configuration block - irrelevant to name mapping.
477
555
  break;
478
556
  default:
479
557
  ast.warnings.push(`Skipped unsupported block "${block.keyword} ${block.name}".`);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * turbine-orm CLI: fixed-window rate limiter
3
+ *
4
+ * A pure leaf module (same role as `cli/destructive.ts`): no imports beyond a
5
+ * Node type, no I/O, no state of its own. Both local servers, Studio and
6
+ * Observe, throttle through it so they behave identically without either one
7
+ * importing the other. Studio in particular statically pulls in the embedded
8
+ * UI, the demo store, PII redaction and QueryInterface, none of which Observe
9
+ * has any use for.
10
+ */
11
+ import type { IncomingMessage } from 'node:http';
12
+ export declare const RATE_LIMIT_WINDOW_MS = 60000;
13
+ export declare const RATE_LIMIT_MAX_REQUESTS = 100;
14
+ /**
15
+ * Bucket identity for one caller. These are loopback tools with a single shared
16
+ * token, so the remote address is the only thing that distinguishes one client
17
+ * from another; requests with no socket (in-process dispatch) share one key.
18
+ */
19
+ export declare function callerKey(req: IncomingMessage): string;
20
+ export interface RateLimitResult {
21
+ allowed: boolean;
22
+ resetAt: number;
23
+ }
24
+ /**
25
+ * Fixed-window counter. `limiter` is the caller's own state map (each server
26
+ * owns one, so exhausting one server's budget never touches the other's) and
27
+ * `token` is the bucket key, not a credential.
28
+ */
29
+ export declare function checkRateLimit(limiter: Map<string, {
30
+ count: number;
31
+ resetAt: number;
32
+ }>, token: string): RateLimitResult;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm CLI: fixed-window rate limiter
4
+ *
5
+ * A pure leaf module (same role as `cli/destructive.ts`): no imports beyond a
6
+ * Node type, no I/O, no state of its own. Both local servers, Studio and
7
+ * Observe, throttle through it so they behave identically without either one
8
+ * importing the other. Studio in particular statically pulls in the embedded
9
+ * UI, the demo store, PII redaction and QueryInterface, none of which Observe
10
+ * has any use for.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.RATE_LIMIT_MAX_REQUESTS = exports.RATE_LIMIT_WINDOW_MS = void 0;
14
+ exports.callerKey = callerKey;
15
+ exports.checkRateLimit = checkRateLimit;
16
+ exports.RATE_LIMIT_WINDOW_MS = 60_000; // 60 seconds
17
+ exports.RATE_LIMIT_MAX_REQUESTS = 100;
18
+ /**
19
+ * Bucket identity for one caller. These are loopback tools with a single shared
20
+ * token, so the remote address is the only thing that distinguishes one client
21
+ * from another; requests with no socket (in-process dispatch) share one key.
22
+ */
23
+ function callerKey(req) {
24
+ return req.socket?.remoteAddress ?? 'unknown';
25
+ }
26
+ /**
27
+ * Fixed-window counter. `limiter` is the caller's own state map (each server
28
+ * owns one, so exhausting one server's budget never touches the other's) and
29
+ * `token` is the bucket key, not a credential.
30
+ */
31
+ function checkRateLimit(limiter, token) {
32
+ const now = Date.now();
33
+ const entry = limiter.get(token);
34
+ if (!entry || now >= entry.resetAt) {
35
+ // Start a new window
36
+ const resetAt = now + exports.RATE_LIMIT_WINDOW_MS;
37
+ limiter.set(token, { count: 1, resetAt });
38
+ return { allowed: true, resetAt };
39
+ }
40
+ entry.count++;
41
+ if (entry.count > exports.RATE_LIMIT_MAX_REQUESTS) {
42
+ return { allowed: false, resetAt: entry.resetAt };
43
+ }
44
+ return { allowed: true, resetAt: entry.resetAt };
45
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * turbine-orm CLI: Studio demo mode (`turbine studio --demo`)
3
+ *
4
+ * Boots Studio with NO database and NO DATABASE_URL: a baked-in, seeded sample
5
+ * dataset served from an in-memory engine. It is the "feel the product in 10
6
+ * seconds" experience: read mode, PII redaction, and the single-row write flow,
7
+ * all safely fake.
8
+ *
9
+ * The store is backed by Turbine's OWN SQLite engine over `node:sqlite`'s
10
+ * `:memory:` database (a built-in on Node >= 22.5, zero new dependency). Because
11
+ * `:memory:` is per-handle, the store dies with the process and every launch
12
+ * starts pristine: writes genuinely apply (edits stick, a refresh shows them)
13
+ * but nothing is ever persisted anywhere.
14
+ *
15
+ * This module lives under `src/cli/` (coverage-excluded, never imported by
16
+ * library code) and reuses `SqlitePool` + `sqliteDialect` from `../sqlite.js`;
17
+ * it never writes its own SQL evaluator.
18
+ */
19
+ import type { PgCompatPool } from '../client.js';
20
+ import type { Dialect } from '../dialect.js';
21
+ import type { SchemaMetadata } from '../schema.js';
22
+ /**
23
+ * The seeded sample schema. Four tables with realistic relations; `email` and
24
+ * `phone` are tagged `pii` so Studio's redaction path is exercised out of the
25
+ * box.
26
+ */
27
+ export declare const DEMO_SCHEMA: SchemaMetadata;
28
+ export interface DemoContext {
29
+ /** In-memory SQLite pool (pg-compatible) backing the demo store. */
30
+ pool: PgCompatPool;
31
+ /** The seeded sample schema metadata. */
32
+ metadata: SchemaMetadata;
33
+ /** The SQLite dialect the Studio handlers compile against in demo mode. */
34
+ dialect: Dialect;
35
+ }
36
+ /**
37
+ * Open a fresh, seeded in-memory demo store and return the pool + metadata +
38
+ * dialect Studio needs. Each call yields an independent, pristine database
39
+ * (`:memory:` is per-handle), so demo launches never share state.
40
+ *
41
+ * @throws Error on Node < 22.5 (no built-in `node:sqlite`).
42
+ */
43
+ export declare function createDemoContext(): DemoContext;
@@ -0,0 +1 @@
1
+ export declare const STUDIO_HTML: string;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * turbine-orm CLI: Studio
3
+ *
4
+ * A local web UI for browsing databases, exploring relations, and composing
5
+ * queries visually. ORM-native since v0.19: there is no raw-SQL input surface.
6
+ * The Query tab builds `findMany` args that are validated against introspected
7
+ * metadata and compiled by QueryInterface (`/api/builder`). Pure Node (built-in
8
+ * `http` module), no runtime dependencies beyond `pg`. CLI defaults to 127.0.0.1
9
+ * and refuses non-loopback hosts unless `npx turbine studio --allow-remote`.
10
+ *
11
+ * Read-only by default. `turbine studio --write` opts in to single-row writes
12
+ * (see the write model below); without the flag the write API routes do not
13
+ * exist (they 404) and the UI renders no write affordances.
14
+ *
15
+ * Security model:
16
+ * • Loopback by default; CLI refuses non-loopback without --allow-remote
17
+ * • Random auth token generated per process, required in Cookie header
18
+ * • No SQL input surface at all: every identifier in a builder or write
19
+ * request is validated against the introspected schema; all values are
20
+ * $N params compiled through the query builders
21
+ * • Read routes run in a READ ONLY transaction (belt-and-suspenders)
22
+ * • Write routes (only when `--write` is set) run in a plain BEGIN/COMMIT
23
+ * transaction, require a matching Origin header (CSRF), and address every
24
+ * row by its full primary key (single row, or a capped `rows` array of
25
+ * PK-addressed statements run atomically)
26
+ * • 30s statement timeout via parameterized set_config()
27
+ * • Per-session rate limiting, cross-origin refusal, security headers, and a
28
+ * per-request CSP nonce for the inline script (no `unsafe-inline`)
29
+ *
30
+ * PII: columns tagged `pii` in code-first metadata are redacted server-side in
31
+ * every row-bearing response (the literal `•• redacted ••`) unless the server
32
+ * was started with `--show-pii`.
33
+ *
34
+ * Write model (opt-in): update a single row; insert/delete one row or a capped
35
+ * list of PK-addressed rows in one all-or-nothing transaction. DDL and
36
+ * predicate-based (unconditional) writes are deliberately unsupported. Use the
37
+ * CLI or migrate for schema changes and true bulk operations.
38
+ */
39
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
40
+ import type { PgCompatPool } from '../client.js';
41
+ import type { Dialect } from '../dialect.js';
42
+ import type { SchemaMetadata, TableMetadata } from '../schema.js';
43
+ export interface StudioOptions {
44
+ url: string;
45
+ schema: string;
46
+ port: number;
47
+ host: string;
48
+ openBrowser: boolean;
49
+ include?: string[];
50
+ exclude?: string[];
51
+ /** Directory where studio-queries.json is persisted. Defaults to `.turbine/` in cwd. */
52
+ stateDir?: string;
53
+ /** Database adapter for dialect-specific behavior (e.g. statement timeout syntax). */
54
+ adapter?: import('../adapters/index.js').DatabaseAdapter;
55
+ /**
56
+ * Opt in to single-row write routes (`/api/row/update|insert|delete`) and the
57
+ * write UI. Default `false`: read-only, with the write routes absent (404).
58
+ */
59
+ write?: boolean;
60
+ /**
61
+ * Reveal PII-tagged column values instead of redacting them. Default `false`.
62
+ */
63
+ showPii?: boolean;
64
+ /**
65
+ * Boot with a seeded, in-memory sample database instead of connecting to a
66
+ * real one (no DATABASE_URL required). Backed by Turbine's own SQLite engine
67
+ * over `node:sqlite` `:memory:`; nothing is ever persisted. Enables the live
68
+ * three-mode switcher (`/api/demo/mode`). Default `false`.
69
+ */
70
+ demo?: boolean;
71
+ /**
72
+ * Directory holding generated Turbine metadata (`turbine generate`'s `out`).
73
+ * PII tags are code-first declarations that introspection never sets, so
74
+ * without this Studio's redaction has nothing to redact against a real
75
+ * database. Read as text; nothing from it is executed. See `pii-tags.ts`.
76
+ */
77
+ metadataDir?: string;
78
+ }
79
+ export interface StudioHandle {
80
+ /** Shut down the server + pool cleanly. */
81
+ dispose: () => Promise<void>;
82
+ /**
83
+ * Where PII tags came from, so the CLI can say so at startup. `null` when no
84
+ * generated metadata was found: redaction is then inert and the user needs to
85
+ * know that rather than assume protection.
86
+ */
87
+ piiTags: {
88
+ path: string;
89
+ applied: number;
90
+ } | null;
91
+ /** Random per-process session token the UI sends via cookie. */
92
+ authToken: string;
93
+ /** Full URL including `?token=...`, safe to print for the user. */
94
+ url: string;
95
+ }
96
+ export interface StudioContext {
97
+ /**
98
+ * The pg-compatible pool. A real `pg.Pool` in normal mode, a `SqlitePool`
99
+ * over an in-memory database in demo mode. Typed as the minimal
100
+ * `PgCompatPool` contract so both shapes work through one code path (pg.Pool
101
+ * satisfies it; the query builders take `pg.Pool` via a cast, matching the
102
+ * external-pool seam in client.ts).
103
+ */
104
+ pool: PgCompatPool;
105
+ metadata: SchemaMetadata;
106
+ options: StudioOptions;
107
+ authToken: string;
108
+ stateDir: string;
109
+ /** Resolved statement timeout (adapter-aware): parameterized SQL + values. */
110
+ statementTimeout: {
111
+ sql: string;
112
+ params: unknown[];
113
+ };
114
+ /** Rate limiter state: tracks requests per authenticated session. */
115
+ rateLimiter: Map<string, {
116
+ count: number;
117
+ resetAt: number;
118
+ }>;
119
+ /**
120
+ * True when write mode is enabled (`--write`): the `/api/row/*` routes exist
121
+ * and the UI renders write affordances. Absent/false → read-only. In demo
122
+ * mode this is toggled live by the `/api/demo/mode` switcher.
123
+ */
124
+ writable?: boolean;
125
+ /** True when PII redaction is disabled (`--show-pii`). Absent/false → redact. */
126
+ showPii?: boolean;
127
+ /**
128
+ * True when Studio is running against the seeded in-memory demo store
129
+ * (`--demo`). Branches the handful of Postgres-specific statements onto their
130
+ * SQLite equivalents and enables the `/api/demo/mode` live switcher.
131
+ */
132
+ demo?: boolean;
133
+ /**
134
+ * SQL dialect the builder/write handlers compile against. Absent → Postgres
135
+ * (the default). Set to the SQLite dialect in demo mode.
136
+ */
137
+ dialect?: Dialect;
138
+ /**
139
+ * Demo mode only: saved queries live here instead of on disk, honoring the
140
+ * "nothing you do here is saved anywhere" promise. Absent in normal mode
141
+ * (saved queries persist to `<stateDir>/studio-queries.json`).
142
+ */
143
+ memorySavedQueries?: SavedQueriesFile;
144
+ }
145
+ /**
146
+ * Start the Studio server. Returns a handle with the session token, a pre-built
147
+ * URL (including the token) that the CLI can print, and a disposer.
148
+ *
149
+ * Typical usage from the CLI:
150
+ * const studio = await startStudio(options);
151
+ * console.log(studio.url);
152
+ * process.on('SIGINT', () => studio.dispose().then(() => process.exit(0)));
153
+ */
154
+ export declare function startStudio(options: StudioOptions): Promise<StudioHandle>;
155
+ export declare function handleRequest(req: IncomingMessage, res: ServerResponse, ctx: StudioContext): Promise<void>;
156
+ export declare function apiDemoMode(req: IncomingMessage, res: ServerResponse, ctx: StudioContext): Promise<void>;
157
+ /** One clickable reference from a column on this table to a row in another. */
158
+ export interface ForeignKeyLink {
159
+ /** Column on THIS table whose cell becomes the link. */
160
+ column: string;
161
+ /** Relation the link was derived from. */
162
+ relation: string;
163
+ /** Table the reference points at. */
164
+ targetTable: string;
165
+ /** Column on the target table the value is matched against. */
166
+ targetColumn: string;
167
+ }
168
+ /** One reverse jump: rows in a child table that reference this table. */
169
+ export interface ReferencedByLink {
170
+ /** Column on THIS table whose value the child column matches (usually the PK). */
171
+ column: string;
172
+ /** Relation the link was derived from. */
173
+ relation: string;
174
+ /** Child table holding the referencing rows. */
175
+ targetTable: string;
176
+ /** Referencing column on the child table. */
177
+ targetColumn: string;
178
+ }
179
+ export declare function relationLinksForTable(table: TableMetadata, metadata: SchemaMetadata, showPii: boolean): {
180
+ foreignKeys: ForeignKeyLink[];
181
+ referencedBy: ReferencedByLink[];
182
+ };
183
+ export declare function apiTableRows(res: ServerResponse, ctx: StudioContext, rawTableName: string, params: URLSearchParams): Promise<void>;
184
+ export declare function resolveColumnName(table: TableMetadata, nameOrField: string): string | null;
185
+ export declare function isTextishType(pgType: string): boolean;
186
+ export declare function escapeLikePattern(s: string): string;
187
+ export declare function apiBuilder(req: IncomingMessage, res: ServerResponse, ctx: StudioContext): Promise<void>;
188
+ export declare function apiRowWrite(req: IncomingMessage, res: ServerResponse, ctx: StudioContext, op: 'update' | 'insert' | 'delete'): Promise<void>;
189
+ interface SavedQuery {
190
+ id: string;
191
+ table: string;
192
+ name: string;
193
+ /** Studio only saves visual-builder queries: there is no raw-SQL surface. */
194
+ kind: 'builder';
195
+ args?: unknown;
196
+ createdAt: string;
197
+ }
198
+ interface SavedQueriesFile {
199
+ version: 1;
200
+ queries: SavedQuery[];
201
+ }
202
+ export declare function apiListSavedQueries(res: ServerResponse, ctx: StudioContext, params: URLSearchParams): void;
203
+ export declare function apiCreateSavedQuery(req: IncomingMessage, res: ServerResponse, ctx: StudioContext): Promise<void>;
204
+ export declare function apiDeleteSavedQuery(res: ServerResponse, ctx: StudioContext, id: string): void;
205
+ /** The literal replacement value for a redacted PII cell. */
206
+ export declare const PII_REDACTED = "\u2022\u2022 redacted \u2022\u2022";
207
+ export {};