turbine-orm 0.61.0 → 0.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/mssql.js +29 -7
- 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/mssql.js +29 -7
- 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/cjs/cli/index.js
CHANGED
|
@@ -69,11 +69,15 @@ exports.tsxInstallCommand = tsxInstallCommand;
|
|
|
69
69
|
exports.tsxRequiredNotice = tsxRequiredNotice;
|
|
70
70
|
exports.detectConsumerModuleType = detectConsumerModuleType;
|
|
71
71
|
exports.planInitSteps = planInitSteps;
|
|
72
|
+
exports.gitignoreIgnoresEnv = gitignoreIgnoresEnv;
|
|
73
|
+
exports.planEnvScaffold = planEnvScaffold;
|
|
74
|
+
exports.scaffoldEnvForUrl = scaffoldEnvForUrl;
|
|
72
75
|
exports.initEnvNotice = initEnvNotice;
|
|
73
76
|
exports.resolveMigrateFromPrismaUrl = resolveMigrateFromPrismaUrl;
|
|
74
77
|
exports.buildMigrateDeployOptions = buildMigrateDeployOptions;
|
|
75
78
|
exports.getSeedExecutionPlan = getSeedExecutionPlan;
|
|
76
79
|
exports.isLoopbackHost = isLoopbackHost;
|
|
80
|
+
exports.showSubcommandHelp = showSubcommandHelp;
|
|
77
81
|
const node_fs_1 = require("node:fs");
|
|
78
82
|
const node_os_1 = require("node:os");
|
|
79
83
|
const node_path_1 = require("node:path");
|
|
@@ -98,6 +102,21 @@ const prisma_resolve_js_1 = require("./prisma-resolve.js");
|
|
|
98
102
|
const prisma_schema_js_1 = require("./prisma-schema.js");
|
|
99
103
|
const studio_js_1 = require("./studio.js");
|
|
100
104
|
const ui_js_1 = require("./ui.js");
|
|
105
|
+
/**
|
|
106
|
+
* Fail an argument-parse error the way every other CLI failure looks: banner,
|
|
107
|
+
* red error line, then indented hint lines. The flag validators used to call
|
|
108
|
+
* `console.error` directly, so a typo in `--recipe` printed one bare unstyled
|
|
109
|
+
* sentence with no banner and no hint, which reads like an internal crash
|
|
110
|
+
* rather than "you forgot the recipe name".
|
|
111
|
+
*/
|
|
112
|
+
function failArg(message, ...hints) {
|
|
113
|
+
(0, ui_js_1.banner)();
|
|
114
|
+
(0, ui_js_1.error)(message);
|
|
115
|
+
for (const hint of hints)
|
|
116
|
+
console.log(` ${(0, ui_js_1.dim)(hint)}`);
|
|
117
|
+
(0, ui_js_1.newline)();
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
101
120
|
function parseArgs(argv = process.argv.slice(2)) {
|
|
102
121
|
const args = argv;
|
|
103
122
|
const result = {
|
|
@@ -158,6 +177,9 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
158
177
|
case '--skip-schema':
|
|
159
178
|
result.skipSchema = true;
|
|
160
179
|
break;
|
|
180
|
+
case '--with-schema':
|
|
181
|
+
result.withSchema = true;
|
|
182
|
+
break;
|
|
161
183
|
case '--skip-seed':
|
|
162
184
|
result.skipSeed = true;
|
|
163
185
|
break;
|
|
@@ -211,8 +233,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
211
233
|
case '--import-ext':
|
|
212
234
|
case '--import-extension':
|
|
213
235
|
if (next !== 'js' && next !== 'none' && next !== 'auto') {
|
|
214
|
-
|
|
215
|
-
process.exit(1);
|
|
236
|
+
failArg(`${(0, ui_js_1.cyan)('--import-ext')} requires one of: js, none, auto ${(0, ui_js_1.dim)(`(got ${next ?? 'nothing'})`)}`, 'Example: npx turbine generate --import-ext none');
|
|
216
237
|
}
|
|
217
238
|
result.importExtension = next;
|
|
218
239
|
i++;
|
|
@@ -228,8 +249,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
228
249
|
break;
|
|
229
250
|
case '--recipe':
|
|
230
251
|
if (next === undefined || next.startsWith('-')) {
|
|
231
|
-
|
|
232
|
-
process.exit(1);
|
|
252
|
+
failArg(`${(0, ui_js_1.cyan)('--recipe')} requires a recipe name.`, `Known recipes: ${Object.keys(migrate_js_1.MIGRATION_RECIPES).join(', ') || '(none)'}`, 'Example: npx turbine migrate create backfill_full_name --recipe backfill');
|
|
233
253
|
}
|
|
234
254
|
result.recipe = next;
|
|
235
255
|
i++;
|
|
@@ -633,6 +653,23 @@ function planInitSteps(state, flags) {
|
|
|
633
653
|
return { id, action: 'skip', defaultYes: true, skipReason: 'flag' };
|
|
634
654
|
if (exists)
|
|
635
655
|
return { id, action: 'skip', defaultYes: true, skipReason: 'exists' };
|
|
656
|
+
// An EMPTY code-first schema file written next to a database that ALREADY
|
|
657
|
+
// has tables is what makes the very next `turbine push` announce "Schema
|
|
658
|
+
// defines 0 tables" and list every real table under "Extra tables in
|
|
659
|
+
// database (not dropped automatically)", which reads like imminent data
|
|
660
|
+
// loss. For these projects the database is the source of truth
|
|
661
|
+
// (`turbine generate`), so the default flips to "don't scaffold it":
|
|
662
|
+
// prompted (default no) on a TTY, skipped with an explanation otherwise.
|
|
663
|
+
//
|
|
664
|
+
// `--with-schema` opts back in and returns this step to the normal path. A
|
|
665
|
+
// code-first project bootstrapping in CI against a populated database is a
|
|
666
|
+
// real case, and a default that no flag can override is a behavior change
|
|
667
|
+
// with no way out, not a safety rail.
|
|
668
|
+
if (id === 'schema' && state.dbHasTables === true && !flags.withSchema) {
|
|
669
|
+
return mode === 'prompt'
|
|
670
|
+
? { id, action: 'prompt', defaultYes: false }
|
|
671
|
+
: { id, action: 'skip', defaultYes: false, skipReason: 'db-has-tables' };
|
|
672
|
+
}
|
|
636
673
|
return { id, action: mode === 'prompt' ? 'prompt' : 'run', defaultYes: true };
|
|
637
674
|
};
|
|
638
675
|
steps.push(scaffold('schema', state.schemaExists, flags.skipSchema));
|
|
@@ -687,17 +724,33 @@ async function promptYesNo(question, defaultYes) {
|
|
|
687
724
|
rl.close();
|
|
688
725
|
}
|
|
689
726
|
}
|
|
690
|
-
/**
|
|
691
|
-
|
|
727
|
+
/**
|
|
728
|
+
* Probe database reachability (and how populated it is) with a short-lived
|
|
729
|
+
* connection. Never throws: an unreachable database is a normal init state.
|
|
730
|
+
*/
|
|
731
|
+
async function probeDatabase(url, schema) {
|
|
692
732
|
try {
|
|
693
733
|
const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
694
734
|
const client = new pg.Client({ connectionString: url });
|
|
695
735
|
await client.connect();
|
|
736
|
+
let tableCount = 0;
|
|
737
|
+
try {
|
|
738
|
+
const res = await client.query(`SELECT count(*)::text AS count
|
|
739
|
+
FROM information_schema.tables
|
|
740
|
+
WHERE table_schema = $1
|
|
741
|
+
AND table_type = 'BASE TABLE'
|
|
742
|
+
AND table_name NOT LIKE '\\_turbine\\_%'`, [schema]);
|
|
743
|
+
tableCount = Number.parseInt(res.rows[0]?.count ?? '0', 10) || 0;
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
// Reachable but the catalog read failed (permissions, odd engine). Treat
|
|
747
|
+
// it as "unknown", not "empty": tableCount 0 only suppresses a scaffold.
|
|
748
|
+
}
|
|
696
749
|
await client.end();
|
|
697
|
-
return true;
|
|
750
|
+
return { reachable: true, tableCount };
|
|
698
751
|
}
|
|
699
752
|
catch {
|
|
700
|
-
return false;
|
|
753
|
+
return { reachable: false, tableCount: 0 };
|
|
701
754
|
}
|
|
702
755
|
}
|
|
703
756
|
const INIT_SEED_TEMPLATE = `/**
|
|
@@ -738,6 +791,132 @@ export default defineSchema({
|
|
|
738
791
|
// },
|
|
739
792
|
});
|
|
740
793
|
`;
|
|
794
|
+
/**
|
|
795
|
+
* Does this `.gitignore` text already ignore `.env`?
|
|
796
|
+
*
|
|
797
|
+
* Line-based rather than a substring search: a `.gitignore` mentioning
|
|
798
|
+
* `.env.example` (a very common line, since the example file is the one you DO
|
|
799
|
+
* commit) contains the text `.env` while ignoring nothing of the kind, and
|
|
800
|
+
* treating that as covered is how the password would stay committable.
|
|
801
|
+
* Negations (`!.env`) are honored as a later line overriding an earlier one,
|
|
802
|
+
* exactly as git resolves them.
|
|
803
|
+
*
|
|
804
|
+
* @internal exported for tests.
|
|
805
|
+
*/
|
|
806
|
+
function gitignoreIgnoresEnv(content) {
|
|
807
|
+
let ignored = false;
|
|
808
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
809
|
+
const line = raw.trim();
|
|
810
|
+
if (line === '' || line.startsWith('#'))
|
|
811
|
+
continue;
|
|
812
|
+
const negated = line.startsWith('!');
|
|
813
|
+
// A leading `/` anchors to the repo root, a trailing `/` means directory.
|
|
814
|
+
const pattern = (negated ? line.slice(1) : line).replace(/^\//, '').replace(/\/$/, '');
|
|
815
|
+
// The patterns that actually match a root-level `.env` file.
|
|
816
|
+
if (pattern === '.env' || pattern === '.env*' || pattern === '*.env' || pattern === '**/.env') {
|
|
817
|
+
ignored = !negated;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return ignored;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Decide what the `--url`-carries-a-password scaffold writes. Pure, so the whole
|
|
824
|
+
* matrix (fresh project, existing `.env`, existing `.gitignore`, re-run) is
|
|
825
|
+
* testable without a filesystem.
|
|
826
|
+
*
|
|
827
|
+
* An existing `DATABASE_URL` in `.env` is NEVER rewritten: it is the value the
|
|
828
|
+
* project already runs against, and silently repointing it at the `--url` from
|
|
829
|
+
* one command line is a worse failure than printing a notice.
|
|
830
|
+
*
|
|
831
|
+
* @internal exported for tests.
|
|
832
|
+
*/
|
|
833
|
+
function planEnvScaffold(state) {
|
|
834
|
+
return {
|
|
835
|
+
env: !state.envExists ? 'created' : state.envHasDatabaseUrl ? 'unchanged' : 'appended',
|
|
836
|
+
envExample: state.envExampleExists ? 'unchanged' : 'created',
|
|
837
|
+
// No `.gitignore` at all means nothing is ignored, so the `.env` we just
|
|
838
|
+
// wrote is committable. Create one rather than warn about it.
|
|
839
|
+
gitignore: !state.gitignoreExists ? 'created' : state.gitignoreIgnoresEnv ? 'unchanged' : 'appended',
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const ENV_EXAMPLE_TEMPLATE = `# Copy this file to .env and fill in your own values.
|
|
843
|
+
# This file is committed; .env is not.
|
|
844
|
+
DATABASE_URL=postgres://user:password@localhost:5432/database
|
|
845
|
+
`;
|
|
846
|
+
/**
|
|
847
|
+
* Move a password-bearing `--url` out of `turbine.config.ts` and into `.env`.
|
|
848
|
+
*
|
|
849
|
+
* `turbine init --url postgres://user:PASSWORD@host/db` is the documented
|
|
850
|
+
* one-liner, and it used to inline that string verbatim into a file projects
|
|
851
|
+
* commit, with no `.gitignore` written at all. The config template now refuses
|
|
852
|
+
* to inline a secret (see `configTemplate`), so the real value has to land
|
|
853
|
+
* somewhere the config can read it from: this writes `.env`, scaffolds the
|
|
854
|
+
* committable `.env.example` next to it, and makes sure `.gitignore` covers
|
|
855
|
+
* `.env` BEFORE the secret is on disk long enough to be staged.
|
|
856
|
+
*
|
|
857
|
+
* Paths are cwd-relative, exactly like the rest of the init scaffold.
|
|
858
|
+
*
|
|
859
|
+
* @internal exported for tests.
|
|
860
|
+
*/
|
|
861
|
+
function scaffoldEnvForUrl(url) {
|
|
862
|
+
const envPath = '.env';
|
|
863
|
+
const envExamplePath = '.env.example';
|
|
864
|
+
const gitignorePath = '.gitignore';
|
|
865
|
+
const envExists = (0, node_fs_1.existsSync)(envPath);
|
|
866
|
+
const plan = planEnvScaffold({
|
|
867
|
+
envExists,
|
|
868
|
+
envHasDatabaseUrl: envExists ? /^\s*(export\s+)?DATABASE_URL\s*=/m.test((0, node_fs_1.readFileSync)(envPath, 'utf-8')) : false,
|
|
869
|
+
envExampleExists: (0, node_fs_1.existsSync)(envExamplePath),
|
|
870
|
+
gitignoreExists: (0, node_fs_1.existsSync)(gitignorePath),
|
|
871
|
+
gitignoreIgnoresEnv: (0, node_fs_1.existsSync)(gitignorePath) ? gitignoreIgnoresEnv((0, node_fs_1.readFileSync)(gitignorePath, 'utf-8')) : false,
|
|
872
|
+
});
|
|
873
|
+
// .gitignore FIRST: the ignore rule has to exist before the file holding the
|
|
874
|
+
// password does, or a `git add -A` in between commits it.
|
|
875
|
+
if (plan.gitignore === 'created')
|
|
876
|
+
(0, node_fs_1.writeFileSync)(gitignorePath, '# Local environment (secrets)\n.env\n', 'utf-8');
|
|
877
|
+
else if (plan.gitignore === 'appended')
|
|
878
|
+
(0, node_fs_1.appendFileSync)(gitignorePath, '\n# Local environment (secrets)\n.env\n');
|
|
879
|
+
if (plan.env === 'created') {
|
|
880
|
+
// 0600 on CREATION only. The whole point of this scaffold is moving a live
|
|
881
|
+
// password out of a committed file, and the default 0644 would leave it
|
|
882
|
+
// world-readable to every account on a shared box or CI runner. `mode` is
|
|
883
|
+
// applied by the OS only when the open() actually creates the file, so if
|
|
884
|
+
// one appeared since the existsSync above we write into it without widening
|
|
885
|
+
// or narrowing whatever mode its owner chose.
|
|
886
|
+
(0, node_fs_1.writeFileSync)(envPath, `# Turbine database connection. Never commit this file.\nDATABASE_URL=${url}\n`, {
|
|
887
|
+
encoding: 'utf-8',
|
|
888
|
+
mode: 0o600,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
else if (plan.env === 'appended') {
|
|
892
|
+
(0, node_fs_1.appendFileSync)(envPath, `\n# Added by turbine init\nDATABASE_URL=${url}\n`);
|
|
893
|
+
}
|
|
894
|
+
if (plan.envExample === 'created')
|
|
895
|
+
(0, node_fs_1.writeFileSync)(envExamplePath, ENV_EXAMPLE_TEMPLATE, 'utf-8');
|
|
896
|
+
return plan;
|
|
897
|
+
}
|
|
898
|
+
/** Say exactly what was written where, so nobody has to guess where the password went. */
|
|
899
|
+
function reportEnvScaffold(plan, url) {
|
|
900
|
+
(0, ui_js_1.warn)(`The connection string passed to ${(0, ui_js_1.cyan)('--url')} contains a password.`);
|
|
901
|
+
console.log(` ${(0, ui_js_1.dim)('It was NOT written into')} ${(0, ui_js_1.cyan)('turbine.config.ts')}${(0, ui_js_1.dim)(': that file reads')} ${(0, ui_js_1.cyan)('process.env.DATABASE_URL')}${(0, ui_js_1.dim)('.')}`);
|
|
902
|
+
if (plan.env === 'created')
|
|
903
|
+
(0, ui_js_1.success)(`Wrote the connection string to ${(0, ui_js_1.cyan)('.env')} ${(0, ui_js_1.dim)(`(${(0, ui_js_1.redactUrl)(url)})`)}`);
|
|
904
|
+
else if (plan.env === 'appended')
|
|
905
|
+
(0, ui_js_1.success)(`Appended ${(0, ui_js_1.cyan)('DATABASE_URL')} to your existing ${(0, ui_js_1.cyan)('.env')} ${(0, ui_js_1.dim)(`(${(0, ui_js_1.redactUrl)(url)})`)}`);
|
|
906
|
+
else
|
|
907
|
+
(0, ui_js_1.info)(`Your ${(0, ui_js_1.cyan)('.env')} already sets ${(0, ui_js_1.cyan)('DATABASE_URL')}: left untouched. The ${(0, ui_js_1.cyan)('--url')} value was not saved anywhere.`);
|
|
908
|
+
if (plan.envExample === 'created')
|
|
909
|
+
(0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)('.env.example')} ${(0, ui_js_1.dim)('(safe to commit)')}`);
|
|
910
|
+
if (plan.gitignore === 'created')
|
|
911
|
+
(0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)('.gitignore')} ignoring ${(0, ui_js_1.cyan)('.env')}`);
|
|
912
|
+
else if (plan.gitignore === 'appended')
|
|
913
|
+
(0, ui_js_1.success)(`Added ${(0, ui_js_1.cyan)('.env')} to ${(0, ui_js_1.cyan)('.gitignore')}`);
|
|
914
|
+
else
|
|
915
|
+
(0, ui_js_1.info)(`${(0, ui_js_1.cyan)('.gitignore')} already ignores ${(0, ui_js_1.cyan)('.env')}`);
|
|
916
|
+
if (plan.env !== 'unchanged') {
|
|
917
|
+
console.log(` ${(0, ui_js_1.dim)('The password is now in')} ${(0, ui_js_1.cyan)('.env')}${(0, ui_js_1.dim)('. If it was ever pushed to a shared history, rotate it.')}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
741
920
|
/** Create supporting directories + .gitignore entries (unconditional, unprompted). */
|
|
742
921
|
function ensureInitScaffoldDirs(config) {
|
|
743
922
|
const migrDir = config.migrationsDir;
|
|
@@ -892,15 +1071,27 @@ function reportInitSkip(step) {
|
|
|
892
1071
|
case 'default-no':
|
|
893
1072
|
console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.dot} ${label} skipped (default no)`)}`);
|
|
894
1073
|
break;
|
|
1074
|
+
case 'db-has-tables':
|
|
1075
|
+
(0, ui_js_1.info)(`${label} not created: your database already has tables ${(0, ui_js_1.dim)('- skipped')}`);
|
|
1076
|
+
console.log(` ${(0, ui_js_1.dim)('An empty')} ${(0, ui_js_1.cyan)('defineSchema()')} ${(0, ui_js_1.dim)('file would make')} ${(0, ui_js_1.cyan)('turbine push')} ${(0, ui_js_1.dim)('report every existing')}`);
|
|
1077
|
+
console.log(` ${(0, ui_js_1.dim)('table as "extra". Your database is the source of truth here: run')} ${(0, ui_js_1.cyan)('turbine generate')}`);
|
|
1078
|
+
console.log(` ${(0, ui_js_1.dim)('to produce the typed client from it. For a code-first schema instead, re-run')}`);
|
|
1079
|
+
console.log(` ${(0, ui_js_1.dim)('with')} ${(0, ui_js_1.cyan)('--with-schema')} ${(0, ui_js_1.dim)('to scaffold')} ${(0, ui_js_1.cyan)('turbine/schema.ts')} ${(0, ui_js_1.dim)('anyway, then use')} ${(0, ui_js_1.cyan)('turbine push')}${(0, ui_js_1.dim)('.')}`);
|
|
1080
|
+
break;
|
|
895
1081
|
default:
|
|
896
1082
|
console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.dot} ${label} skipped`)}`);
|
|
897
1083
|
}
|
|
898
1084
|
}
|
|
899
1085
|
/** The interactive prompt question for a promptable step. */
|
|
900
|
-
function initPromptQuestion(step, config, seedFilePath) {
|
|
1086
|
+
function initPromptQuestion(step, config, seedFilePath, dbTableCount = 0) {
|
|
901
1087
|
switch (step.id) {
|
|
902
1088
|
case 'schema':
|
|
903
|
-
|
|
1089
|
+
// Ask the honest question when the database is already populated: the
|
|
1090
|
+
// file would be EMPTY, and `turbine push` reports every existing table as
|
|
1091
|
+
// extra until it is filled in by hand.
|
|
1092
|
+
return dbTableCount > 0
|
|
1093
|
+
? `Your database already has ${dbTableCount} table(s). Create an EMPTY code-first schema file anyway (${config.schemaFile})?`
|
|
1094
|
+
: `Create a starter schema file (${config.schemaFile})?`;
|
|
904
1095
|
case 'seed-file':
|
|
905
1096
|
return `Create a starter seed file (${seedFilePath})?`;
|
|
906
1097
|
case 'push':
|
|
@@ -1009,11 +1200,18 @@ async function cmdInit(args, config) {
|
|
|
1009
1200
|
// Probe the database once, up front, so the planner can decide the DB steps.
|
|
1010
1201
|
// Skip the probe entirely when every DB step is already flag-skipped.
|
|
1011
1202
|
const anyDbStepPossible = !(args.skipPush && args.skipGenerate && args.skipSeed);
|
|
1203
|
+
let dbTableCount = 0;
|
|
1012
1204
|
if (hasUrl && anyDbStepPossible) {
|
|
1013
1205
|
const probe = new ui_js_1.Spinner('Checking database connection').start();
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1206
|
+
const probed = await probeDatabase(url, config.schema);
|
|
1207
|
+
state.dbReachable = probed.reachable;
|
|
1208
|
+
dbTableCount = probed.tableCount;
|
|
1209
|
+
state.dbHasTables = probed.tableCount > 0;
|
|
1210
|
+
if (state.dbReachable) {
|
|
1211
|
+
probe.succeed(dbTableCount > 0
|
|
1212
|
+
? `Database is reachable ${(0, ui_js_1.dim)(`(${dbTableCount} table(s) in schema "${config.schema}")`)}`
|
|
1213
|
+
: 'Database is reachable');
|
|
1214
|
+
}
|
|
1017
1215
|
else
|
|
1018
1216
|
probe.info('Database not reachable: push / generate / seed steps will be skipped');
|
|
1019
1217
|
}
|
|
@@ -1022,6 +1220,7 @@ async function cmdInit(args, config) {
|
|
|
1022
1220
|
force: args.force === true,
|
|
1023
1221
|
interactive,
|
|
1024
1222
|
skipSchema: args.skipSchema === true,
|
|
1223
|
+
withSchema: args.withSchema === true,
|
|
1025
1224
|
skipSeed: args.skipSeed === true,
|
|
1026
1225
|
skipPush: args.skipPush === true,
|
|
1027
1226
|
skipGenerate: args.skipGenerate === true,
|
|
@@ -1039,17 +1238,26 @@ async function cmdInit(args, config) {
|
|
|
1039
1238
|
reportInitSkip(step);
|
|
1040
1239
|
continue;
|
|
1041
1240
|
}
|
|
1042
|
-
const shouldRun = step.action === 'run'
|
|
1241
|
+
const shouldRun = step.action === 'run'
|
|
1242
|
+
? true
|
|
1243
|
+
: await promptYesNo(initPromptQuestion(step, config, seedFilePath, dbTableCount), step.defaultYes);
|
|
1043
1244
|
if (!shouldRun) {
|
|
1044
1245
|
(0, ui_js_1.info)(`Skipped ${initStepLabel(step.id)}`);
|
|
1045
1246
|
continue;
|
|
1046
1247
|
}
|
|
1047
1248
|
switch (step.id) {
|
|
1048
|
-
case 'config':
|
|
1249
|
+
case 'config': {
|
|
1049
1250
|
(0, node_fs_1.writeFileSync)('turbine.config.ts', (0, config_js_1.configTemplate)(args.url ?? undefined), 'utf-8');
|
|
1050
1251
|
(0, ui_js_1.success)(state.configExists ? `Overwrote ${(0, ui_js_1.cyan)('turbine.config.ts')}` : `Created ${(0, ui_js_1.cyan)('turbine.config.ts')}`);
|
|
1051
1252
|
tsFilesWritten.push('turbine.config.ts');
|
|
1253
|
+
// A `--url` with a password is never inlined by configTemplate, so the
|
|
1254
|
+
// real value has to be put somewhere the config can read it from.
|
|
1255
|
+
if (args.url && (0, config_js_1.connectionStringHasPassword)(args.url)) {
|
|
1256
|
+
(0, ui_js_1.newline)();
|
|
1257
|
+
reportEnvScaffold(scaffoldEnvForUrl(args.url), args.url);
|
|
1258
|
+
}
|
|
1052
1259
|
break;
|
|
1260
|
+
}
|
|
1053
1261
|
case 'schema':
|
|
1054
1262
|
writeInitSchemaTemplate(config);
|
|
1055
1263
|
if ((0, loader_js_1.needsTsLoader)(config.schemaFile))
|
|
@@ -3138,8 +3346,10 @@ async function cmdStudio(args, config) {
|
|
|
3138
3346
|
(0, ui_js_1.newline)();
|
|
3139
3347
|
process.exit(1);
|
|
3140
3348
|
}
|
|
3141
|
-
|
|
3142
|
-
|
|
3349
|
+
// warn() prints; it returns void. Wrapping it in console.log() printed a
|
|
3350
|
+
// literal grey "undefined" line under every one of these SAFETY warnings.
|
|
3351
|
+
(0, ui_js_1.warn)(`Studio is binding to ${(0, ui_js_1.yellow)(host)}, this is NOT loopback. ` +
|
|
3352
|
+
`Anyone on your network who can reach this port + guess the session token can read your database.`);
|
|
3143
3353
|
}
|
|
3144
3354
|
const spinner = new ui_js_1.Spinner(demo ? 'Seeding demo dataset' : 'Introspecting database').start();
|
|
3145
3355
|
let studio;
|
|
@@ -3185,12 +3395,12 @@ async function cmdStudio(args, config) {
|
|
|
3185
3395
|
// Loud startup warnings for the opt-in modes that widen Studio's surface.
|
|
3186
3396
|
if (args.write) {
|
|
3187
3397
|
(0, ui_js_1.newline)();
|
|
3188
|
-
|
|
3189
|
-
`${(0, ui_js_1.redactUrl)(url)}. Every change is committed directly to your database.`)
|
|
3398
|
+
(0, ui_js_1.warn)('WRITE MODE is ON. Studio can update, insert, and delete single rows in ' +
|
|
3399
|
+
`${(0, ui_js_1.redactUrl)(url)}. Every change is committed directly to your database.`);
|
|
3190
3400
|
}
|
|
3191
3401
|
if (args.showPii) {
|
|
3192
3402
|
(0, ui_js_1.newline)();
|
|
3193
|
-
|
|
3403
|
+
(0, ui_js_1.warn)('--show-pii is ON. PII-tagged column values are shown UNREDACTED in Studio.');
|
|
3194
3404
|
}
|
|
3195
3405
|
// PII tags are a code-first declaration; introspection never infers them.
|
|
3196
3406
|
// Say plainly whether any reached this session, so nobody assumes a
|
|
@@ -3201,9 +3411,9 @@ async function cmdStudio(args, config) {
|
|
|
3201
3411
|
console.log(` ${(0, ui_js_1.dim)('PII redaction:')} ${studio.piiTags.applied} tagged column(s) from ${(0, ui_js_1.dim)(studio.piiTags.path)}`);
|
|
3202
3412
|
}
|
|
3203
3413
|
else {
|
|
3204
|
-
|
|
3414
|
+
(0, ui_js_1.warn)('No PII-tagged columns found, so nothing will be redacted. Tags are declared in code ' +
|
|
3205
3415
|
`(defineSchema \`pii: true\`) and read from generated metadata in ${config.out}; ` +
|
|
3206
|
-
'introspection alone never infers them. Run `turbine generate` after tagging.')
|
|
3416
|
+
'introspection alone never infers them. Run `turbine generate` after tagging.');
|
|
3207
3417
|
}
|
|
3208
3418
|
}
|
|
3209
3419
|
(0, ui_js_1.newline)();
|
|
@@ -3283,8 +3493,9 @@ async function cmdObserve(args) {
|
|
|
3283
3493
|
(0, ui_js_1.newline)();
|
|
3284
3494
|
process.exit(1);
|
|
3285
3495
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3496
|
+
// warn() prints and returns void; see the same guard in cmdStudio.
|
|
3497
|
+
(0, ui_js_1.warn)(`Observe is binding to ${(0, ui_js_1.yellow)(host)}, this is NOT loopback. ` +
|
|
3498
|
+
`Anyone on your network who can reach this port + guess the session token can read your metrics.`);
|
|
3288
3499
|
}
|
|
3289
3500
|
const spinner = new ui_js_1.Spinner('Connecting to metrics database').start();
|
|
3290
3501
|
let handle;
|
|
@@ -3323,6 +3534,13 @@ async function cmdObserve(args) {
|
|
|
3323
3534
|
// ---------------------------------------------------------------------------
|
|
3324
3535
|
// Subcommand help
|
|
3325
3536
|
// ---------------------------------------------------------------------------
|
|
3537
|
+
/**
|
|
3538
|
+
* Print `<command> --help` for a command that has real help, returning whether
|
|
3539
|
+
* one existed. Falling through to the GLOBAL help is the failure mode this map
|
|
3540
|
+
* guards against, and it is silent, so the coverage is asserted in tests.
|
|
3541
|
+
*
|
|
3542
|
+
* @internal exported for tests.
|
|
3543
|
+
*/
|
|
3326
3544
|
function showSubcommandHelp(command) {
|
|
3327
3545
|
const helpMap = {
|
|
3328
3546
|
init: showInitHelp,
|
|
@@ -3334,6 +3552,8 @@ function showSubcommandHelp(command) {
|
|
|
3334
3552
|
migration: showMigrateHelp,
|
|
3335
3553
|
seed: showSeedHelp,
|
|
3336
3554
|
status: showStatusHelp,
|
|
3555
|
+
doctor: showDoctorHelp,
|
|
3556
|
+
studio: showStudioHelp,
|
|
3337
3557
|
mcp: showMcpHelp,
|
|
3338
3558
|
};
|
|
3339
3559
|
const fn = helpMap[command];
|
|
@@ -3363,6 +3583,7 @@ function showInitHelp() {
|
|
|
3363
3583
|
console.log(` ${(0, ui_js_1.cyan)('--force, -f')} Overwrite existing config file`);
|
|
3364
3584
|
console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
|
|
3365
3585
|
console.log(` ${(0, ui_js_1.cyan)('--skip-schema')} Don't create the starter schema file`);
|
|
3586
|
+
console.log(` ${(0, ui_js_1.cyan)('--with-schema')} Create it even if the database already has tables`);
|
|
3366
3587
|
console.log(` ${(0, ui_js_1.cyan)('--skip-seed')} Don't create the seed file or run the seed`);
|
|
3367
3588
|
console.log(` ${(0, ui_js_1.cyan)('--skip-push')} Don't push the schema to the database`);
|
|
3368
3589
|
console.log(` ${(0, ui_js_1.cyan)('--skip-generate')} Don't generate the typed client`);
|
|
@@ -3515,6 +3736,83 @@ function showStatusHelp() {
|
|
|
3515
3736
|
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
3516
3737
|
(0, ui_js_1.newline)();
|
|
3517
3738
|
}
|
|
3739
|
+
function showDoctorHelp() {
|
|
3740
|
+
(0, ui_js_1.banner)();
|
|
3741
|
+
console.log(` ${(0, ui_js_1.bold)('turbine doctor')}, Index + cached-plan triage`);
|
|
3742
|
+
(0, ui_js_1.newline)();
|
|
3743
|
+
console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
|
|
3744
|
+
console.log(` npx turbine doctor ${(0, ui_js_1.dim)('[options]')}`);
|
|
3745
|
+
(0, ui_js_1.newline)();
|
|
3746
|
+
console.log(` Introspects your schema and live statistics, then reports:`);
|
|
3747
|
+
console.log(` ${(0, ui_js_1.dim)('•')} relation probes with no usable index ${(0, ui_js_1.dim)('(ranked by estimated cost)')}`);
|
|
3748
|
+
console.log(` ${(0, ui_js_1.dim)('•')} INVALID indexes ${(0, ui_js_1.dim)('(left behind by a failed CREATE INDEX CONCURRENTLY)')}`);
|
|
3749
|
+
console.log(` ${(0, ui_js_1.dim)('•')} columns whose value distribution can flip a cached plan`);
|
|
3750
|
+
console.log(` ${(0, ui_js_1.dim)('•')} with ${(0, ui_js_1.cyan)('--unused')}: never-scanned and redundant indexes, with DROP suggestions`);
|
|
3751
|
+
(0, ui_js_1.newline)();
|
|
3752
|
+
console.log(` ${(0, ui_js_1.dim)('Read-only: doctor never writes to your database. Only')} ${(0, ui_js_1.cyan)('--fix')} ${(0, ui_js_1.dim)('writes')}`);
|
|
3753
|
+
console.log(` ${(0, ui_js_1.dim)('anything at all, and only a migration FILE you review and run yourself.')}`);
|
|
3754
|
+
(0, ui_js_1.newline)();
|
|
3755
|
+
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
3756
|
+
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
3757
|
+
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
3758
|
+
console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
|
|
3759
|
+
console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
|
|
3760
|
+
console.log(` ${(0, ui_js_1.cyan)('--fix')} Write an add-index migration for the missing-index findings`);
|
|
3761
|
+
console.log(` ${(0, ui_js_1.cyan)('--no-concurrently')} With ${(0, ui_js_1.cyan)('--fix')}: emit plain CREATE INDEX ${(0, ui_js_1.dim)('(default: CONCURRENTLY, no transaction)')}`);
|
|
3762
|
+
console.log(` ${(0, ui_js_1.cyan)('--json')} Emit the versioned machine-readable report and nothing else`);
|
|
3763
|
+
console.log(` ${(0, ui_js_1.cyan)('--unused')} Also report never-scanned / redundant indexes ${(0, ui_js_1.dim)('(no --fix)')}`);
|
|
3764
|
+
console.log(` ${(0, ui_js_1.cyan)('--audit')} Scope the unused report to doctor's own suggested index names`);
|
|
3765
|
+
console.log(` ${(0, ui_js_1.cyan)('--min-scans')} ${(0, ui_js_1.dim)('<n>')} idx_scan below this counts as never-scanned ${(0, ui_js_1.dim)('(default: 1)')}`);
|
|
3766
|
+
console.log(` ${(0, ui_js_1.cyan)('--metrics-url')} ${(0, ui_js_1.dim)('<url>')} Read ${(0, ui_js_1.cyan)('_turbine_metrics')} for the table-heat boost from a separate DB`);
|
|
3767
|
+
console.log(` ${(0, ui_js_1.cyan)('--no-plan-divergence')} Skip the cached-plan divergence section ${(0, ui_js_1.dim)('(and its pg_stats read)')}`);
|
|
3768
|
+
(0, ui_js_1.newline)();
|
|
3769
|
+
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
|
3770
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine doctor`);
|
|
3771
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine doctor --fix`);
|
|
3772
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine doctor --unused --min-scans 5`);
|
|
3773
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine doctor --json > doctor.json`);
|
|
3774
|
+
(0, ui_js_1.newline)();
|
|
3775
|
+
}
|
|
3776
|
+
function showStudioHelp() {
|
|
3777
|
+
(0, ui_js_1.banner)();
|
|
3778
|
+
console.log(` ${(0, ui_js_1.bold)('turbine studio')}, Launch the local database UI`);
|
|
3779
|
+
(0, ui_js_1.newline)();
|
|
3780
|
+
console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
|
|
3781
|
+
console.log(` npx turbine studio ${(0, ui_js_1.dim)('[options]')}`);
|
|
3782
|
+
(0, ui_js_1.newline)();
|
|
3783
|
+
console.log(` A local web UI with Query / Data / Schema tabs. There is no raw-SQL`);
|
|
3784
|
+
console.log(` surface: the Query tab is a visual ${(0, ui_js_1.cyan)('findMany')} builder and every`);
|
|
3785
|
+
console.log(` identifier is validated against the introspected schema.`);
|
|
3786
|
+
(0, ui_js_1.newline)();
|
|
3787
|
+
console.log(` ${(0, ui_js_1.bold)('Defaults are the safe ones:')}`);
|
|
3788
|
+
console.log(` ${(0, ui_js_1.dim)('•')} read-only ${(0, ui_js_1.dim)('(reads run inside BEGIN READ ONLY)')}`);
|
|
3789
|
+
console.log(` ${(0, ui_js_1.dim)('•')} PII-tagged column values redacted server-side`);
|
|
3790
|
+
console.log(` ${(0, ui_js_1.dim)('•')} bound to 127.0.0.1, behind a random per-session token`);
|
|
3791
|
+
(0, ui_js_1.newline)();
|
|
3792
|
+
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
3793
|
+
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
3794
|
+
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
3795
|
+
console.log(` ${(0, ui_js_1.cyan)('--out, -o')} ${(0, ui_js_1.dim)('<dir>')} Generated-metadata dir, source of the PII tags`);
|
|
3796
|
+
console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
|
|
3797
|
+
console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
|
|
3798
|
+
console.log(` ${(0, ui_js_1.cyan)('--port')} ${(0, ui_js_1.dim)('<n>')} HTTP port ${(0, ui_js_1.dim)('(default: 4983)')}`);
|
|
3799
|
+
console.log(` ${(0, ui_js_1.cyan)('--host')} ${(0, ui_js_1.dim)('<addr>')} Bind address ${(0, ui_js_1.dim)('(default: 127.0.0.1)')}`);
|
|
3800
|
+
console.log(` ${(0, ui_js_1.cyan)('--no-open')} Don't auto-open the browser`);
|
|
3801
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-remote')} Allow a non-loopback ${(0, ui_js_1.cyan)('--host')} ${(0, ui_js_1.dim)('(refused without it)')}`);
|
|
3802
|
+
console.log(` ${(0, ui_js_1.cyan)('--write')} Enable single-row update / insert / delete`);
|
|
3803
|
+
console.log(` ${(0, ui_js_1.cyan)('--show-pii')} Show PII-tagged values unredacted`);
|
|
3804
|
+
console.log(` ${(0, ui_js_1.cyan)('--demo')} Seeded in-memory sample database ${(0, ui_js_1.dim)('(no DATABASE_URL)')}`);
|
|
3805
|
+
(0, ui_js_1.newline)();
|
|
3806
|
+
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
|
3807
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine studio`);
|
|
3808
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine studio --demo`);
|
|
3809
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine studio --write --port 5000`);
|
|
3810
|
+
(0, ui_js_1.newline)();
|
|
3811
|
+
console.log(` ${(0, ui_js_1.dim)('--write commits every change directly to the database, and')} ${(0, ui_js_1.cyan)('--show-pii')}`);
|
|
3812
|
+
console.log(` ${(0, ui_js_1.dim)('reveals values Studio otherwise never sends to the browser. Both print a')}`);
|
|
3813
|
+
console.log(` ${(0, ui_js_1.dim)('startup warning and a persistent in-UI banner. Demo mode saves nothing.')}`);
|
|
3814
|
+
(0, ui_js_1.newline)();
|
|
3815
|
+
}
|
|
3518
3816
|
function showMcpHelp() {
|
|
3519
3817
|
(0, ui_js_1.banner)();
|
|
3520
3818
|
console.log(` ${(0, ui_js_1.bold)('turbine mcp')}, Start read-only MCP server over stdio`);
|
|
@@ -3579,6 +3877,7 @@ function showHelp() {
|
|
|
3579
3877
|
console.log(` ${(0, ui_js_1.bold)('Init options:')}`);
|
|
3580
3878
|
console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
|
|
3581
3879
|
console.log(` ${(0, ui_js_1.cyan)('--skip-schema')} Don't scaffold the schema file`);
|
|
3880
|
+
console.log(` ${(0, ui_js_1.cyan)('--with-schema')} Scaffold it even if the database already has tables`);
|
|
3582
3881
|
console.log(` ${(0, ui_js_1.cyan)('--skip-seed')} Don't scaffold or run the seed file`);
|
|
3583
3882
|
console.log(` ${(0, ui_js_1.cyan)('--skip-push')} Don't offer to push the schema to the database`);
|
|
3584
3883
|
console.log(` ${(0, ui_js_1.cyan)('--skip-generate')} Don't offer to generate the typed client`);
|
package/dist/cjs/cli/mcp.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Readable, Writable } from 'node:stream';
|
|
2
|
+
import pg from 'pg';
|
|
2
3
|
import { type ColumnMetadata, type RelationDef } from '../schema.js';
|
|
3
4
|
export interface McpServerOptions {
|
|
4
5
|
url: string;
|
|
@@ -17,6 +18,13 @@ export interface McpServerOptions {
|
|
|
17
18
|
export interface McpTransport {
|
|
18
19
|
input?: Readable;
|
|
19
20
|
output?: Writable;
|
|
21
|
+
/**
|
|
22
|
+
* Pre-built pool, used ONLY by the perimeter tests so they can drive the real
|
|
23
|
+
* JSON-RPC line handler and the real tool handlers with no database (the same
|
|
24
|
+
* reason Studio exports `handleRequest`). Production never sets it: the
|
|
25
|
+
* server builds its own pool from `options.url`.
|
|
26
|
+
*/
|
|
27
|
+
pool?: pg.Pool;
|
|
20
28
|
}
|
|
21
29
|
export interface McpServerHandle {
|
|
22
30
|
dispose(): Promise<void>;
|