turbine-orm 0.65.0 → 0.66.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 +34 -32
- package/dist/adapters/cockroachdb.js +21 -3
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/yugabytedb.js +20 -3
- package/dist/cjs/adapters/cockroachdb.js +21 -3
- package/dist/cjs/adapters/index.d.ts +15 -0
- package/dist/cjs/adapters/yugabytedb.js +20 -3
- package/dist/cjs/cli/destructive.d.ts +18 -4
- package/dist/cjs/cli/destructive.js +230 -122
- package/dist/cjs/cli/index.d.ts +21 -4
- package/dist/cjs/cli/index.js +119 -22
- package/dist/cjs/cli/mcp.d.ts +28 -8
- package/dist/cjs/cli/mcp.js +170 -127
- package/dist/cjs/cli/migrate.d.ts +134 -13
- package/dist/cjs/cli/migrate.js +349 -241
- package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cjs/cli/pii-predicate-guard.js +390 -0
- package/dist/cjs/cli/prisma-resolve.js +75 -4
- package/dist/cjs/cli/prisma-schema.d.ts +17 -1
- package/dist/cjs/cli/prisma-schema.js +83 -17
- package/dist/cjs/cli/sql-statements.d.ts +125 -0
- package/dist/cjs/cli/sql-statements.js +378 -0
- package/dist/cjs/cli/studio.js +49 -118
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +43 -0
- package/dist/cjs/client.js +125 -6
- package/dist/cjs/dialect.d.ts +123 -0
- package/dist/cjs/dialect.js +33 -0
- package/dist/cjs/errors.d.ts +74 -1
- package/dist/cjs/errors.js +239 -25
- package/dist/cjs/index-advisor.d.ts +33 -1
- package/dist/cjs/index-advisor.js +32 -1
- package/dist/cjs/introspect.d.ts +48 -0
- package/dist/cjs/introspect.js +222 -91
- package/dist/cjs/mssql.js +43 -1
- package/dist/cjs/mysql.d.ts +5 -2
- package/dist/cjs/mysql.js +202 -17
- package/dist/cjs/nested-write.js +6 -1
- package/dist/cjs/pipeline-submittable.js +17 -3
- package/dist/cjs/pipeline.js +75 -9
- package/dist/cjs/powdb.d.ts +23 -0
- package/dist/cjs/powdb.js +33 -1
- package/dist/cjs/powql.d.ts +61 -9
- package/dist/cjs/powql.js +186 -49
- package/dist/cjs/prisma-compat.js +160 -41
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +80 -18
- package/dist/cjs/query/batched-loader.d.ts +10 -0
- package/dist/cjs/query/batched-loader.js +268 -7
- package/dist/cjs/query/builder.d.ts +73 -0
- package/dist/cjs/query/builder.js +225 -28
- package/dist/cjs/query/filters.d.ts +162 -0
- package/dist/cjs/query/filters.js +250 -1
- package/dist/cjs/query/relations.d.ts +10 -10
- package/dist/cjs/query/relations.js +93 -12
- package/dist/cjs/query/types.d.ts +14 -1
- package/dist/cjs/query/utils.d.ts +146 -2
- package/dist/cjs/query/utils.js +210 -4
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/where-compile.d.ts +30 -0
- package/dist/cjs/query/where-compile.js +41 -0
- package/dist/cjs/query/where.d.ts +128 -13
- package/dist/cjs/query/where.js +215 -77
- package/dist/cjs/query/writes.d.ts +1 -1
- package/dist/cjs/query/writes.js +39 -15
- package/dist/cjs/schema-builder.d.ts +2 -1
- package/dist/cjs/schema-sql.d.ts +94 -4
- package/dist/cjs/schema-sql.js +506 -30
- package/dist/cjs/schema.d.ts +3 -1
- package/dist/cjs/sqlite.d.ts +6 -0
- package/dist/cjs/sqlite.js +151 -10
- package/dist/cjs/typed-sql.d.ts +29 -1
- package/dist/cjs/typed-sql.js +30 -12
- package/dist/cli/destructive.d.ts +18 -4
- package/dist/cli/destructive.js +229 -121
- package/dist/cli/index.d.ts +21 -4
- package/dist/cli/index.js +120 -24
- package/dist/cli/mcp.d.ts +28 -8
- package/dist/cli/mcp.js +172 -129
- package/dist/cli/migrate.d.ts +134 -13
- package/dist/cli/migrate.js +347 -238
- package/dist/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cli/pii-predicate-guard.js +386 -0
- package/dist/cli/prisma-resolve.js +75 -4
- package/dist/cli/prisma-schema.d.ts +17 -1
- package/dist/cli/prisma-schema.js +83 -17
- package/dist/cli/sql-statements.d.ts +125 -0
- package/dist/cli/sql-statements.js +373 -0
- package/dist/cli/studio.js +49 -118
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +43 -0
- package/dist/client.js +126 -7
- package/dist/dialect.d.ts +123 -0
- package/dist/dialect.js +33 -0
- package/dist/errors.d.ts +74 -1
- package/dist/errors.js +228 -19
- package/dist/index-advisor.d.ts +33 -1
- package/dist/index-advisor.js +31 -1
- package/dist/introspect.d.ts +48 -0
- package/dist/introspect.js +221 -91
- package/dist/mssql.js +44 -2
- package/dist/mysql.d.ts +5 -2
- package/dist/mysql.js +203 -18
- package/dist/nested-write.js +7 -2
- package/dist/pipeline-submittable.js +18 -4
- package/dist/pipeline.js +76 -10
- package/dist/powdb.d.ts +23 -0
- package/dist/powdb.js +33 -2
- package/dist/powql.d.ts +61 -9
- package/dist/powql.js +187 -50
- package/dist/prisma-compat.js +160 -41
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +82 -20
- package/dist/query/batched-loader.d.ts +10 -0
- package/dist/query/batched-loader.js +270 -9
- package/dist/query/builder.d.ts +73 -0
- package/dist/query/builder.js +226 -30
- package/dist/query/filters.d.ts +162 -0
- package/dist/query/filters.js +246 -1
- package/dist/query/relations.d.ts +10 -10
- package/dist/query/relations.js +94 -14
- package/dist/query/types.d.ts +14 -1
- package/dist/query/utils.d.ts +146 -2
- package/dist/query/utils.js +204 -3
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/where-compile.d.ts +30 -0
- package/dist/query/where-compile.js +40 -1
- package/dist/query/where.d.ts +128 -13
- package/dist/query/where.js +216 -80
- package/dist/query/writes.d.ts +1 -1
- package/dist/query/writes.js +40 -16
- package/dist/schema-builder.d.ts +2 -1
- package/dist/schema-sql.d.ts +94 -4
- package/dist/schema-sql.js +505 -30
- package/dist/schema.d.ts +3 -1
- package/dist/sqlite.d.ts +6 -0
- package/dist/sqlite.js +151 -10
- package/dist/typed-sql.d.ts +29 -1
- package/dist/typed-sql.js +30 -12
- package/package.json +6 -4
package/dist/cjs/cli/studio.js
CHANGED
|
@@ -69,6 +69,7 @@ const index_js_1 = require("../query/index.js");
|
|
|
69
69
|
// `ownLookup` is not re-exported from the query barrel, so it is imported from
|
|
70
70
|
// its defining leaf module rather than duplicated here.
|
|
71
71
|
const utils_js_1 = require("../query/utils.js");
|
|
72
|
+
const pii_predicate_guard_js_1 = require("./pii-predicate-guard.js");
|
|
72
73
|
const pii_tags_js_1 = require("./pii-tags.js");
|
|
73
74
|
const rate_limit_js_1 = require("./rate-limit.js");
|
|
74
75
|
const studio_demo_js_1 = require("./studio-demo.js");
|
|
@@ -498,7 +499,7 @@ function relationLinksForTable(table, metadata, showPii) {
|
|
|
498
499
|
const referencedBy = [];
|
|
499
500
|
const seenFkColumns = new Set();
|
|
500
501
|
for (const [name, rel] of Object.entries(table.relations)) {
|
|
501
|
-
const target = metadata.tables
|
|
502
|
+
const target = (0, utils_js_1.ownLookup)(metadata.tables, rel.to);
|
|
502
503
|
if (!target)
|
|
503
504
|
continue;
|
|
504
505
|
if (rel.type === 'belongsTo') {
|
|
@@ -800,18 +801,6 @@ function parseTableFilters(raw, table, redactedPii) {
|
|
|
800
801
|
// ---------------------------------------------------------------------------
|
|
801
802
|
// API: /api/builder: Turbine ORM findMany spec runner
|
|
802
803
|
// ---------------------------------------------------------------------------
|
|
803
|
-
/** Relation-filter wrappers whose body is a clause against the relation's target. */
|
|
804
|
-
const RELATION_FILTER_WRAPPERS = ['some', 'none', 'every', 'is', 'isNot'];
|
|
805
|
-
/**
|
|
806
|
-
* Recursion bound for the PII guard walk.
|
|
807
|
-
*
|
|
808
|
-
* This number is NOT the security boundary: reaching it REFUSES the request
|
|
809
|
-
* (see `assertWithinDepth`). It only bounds the walk on a pathological payload.
|
|
810
|
-
* It sits well above the query builder's own depth-10 relation cap
|
|
811
|
-
* (`CircularRelationError`) and far above any hand-composed boolean nesting, so
|
|
812
|
-
* nothing the builder would accept is refused here for depth alone.
|
|
813
|
-
*/
|
|
814
|
-
const PII_GUARD_MAX_DEPTH = 32;
|
|
815
804
|
/**
|
|
816
805
|
* Refuse a builder query that FILTERS, SORTS, PAGES, or DE-DUPLICATES on a
|
|
817
806
|
* redacted PII column.
|
|
@@ -825,111 +814,39 @@ const PII_GUARD_MAX_DEPTH = 32;
|
|
|
825
814
|
* already refuses its own equivalents (`parseTableFilters`); the builder route
|
|
826
815
|
* has to refuse all of them too.
|
|
827
816
|
*
|
|
828
|
-
*
|
|
829
|
-
* `
|
|
830
|
-
*
|
|
831
|
-
*
|
|
817
|
+
* The WALK lives in `cli/pii-predicate-guard.ts`, shared with the MCP server's
|
|
818
|
+
* `explain_query`, which asks the same question of the same arg tree. It used to
|
|
819
|
+
* be a second hand-written copy here, and the two drifted exactly the way copies
|
|
820
|
+
* do: a hole opened in both and could only have been closed in one. What stays
|
|
821
|
+
* here is the part that is genuinely Studio's: WHICH columns are hidden (a
|
|
822
|
+
* code-first `pii` tag, unless `--show-pii`), and what a refusal looks like (a
|
|
823
|
+
* `ValidationError`, which `/api/builder` renders as a 400).
|
|
832
824
|
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
* wrappers walked the guard off the end of its own recursion and then handed
|
|
836
|
-
* the untouched predicate to the builder.
|
|
825
|
+
* `select` / `omit` are NOT refused: they return values, and those values are
|
|
826
|
+
* redacted on the way out.
|
|
837
827
|
*/
|
|
838
828
|
function assertNoPiiPredicates(args, tableName, metadata, showPii) {
|
|
839
829
|
if (showPii)
|
|
840
830
|
return;
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
`
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
for (const item of node)
|
|
861
|
-
visitClause(item, table, depth);
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
for (const [key, value] of Object.entries(node)) {
|
|
865
|
-
if (key === 'AND' || key === 'OR' || key === 'NOT') {
|
|
866
|
-
visitClause(value, table, depth + 1);
|
|
867
|
-
continue;
|
|
868
|
-
}
|
|
869
|
-
const relation = Object.hasOwn(table.relations, key) ? table.relations[key] : undefined;
|
|
870
|
-
if (relation) {
|
|
871
|
-
visitRelationValue(value, metadata.tables[relation.to], depth + 1);
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
874
|
-
const column = (0, utils_js_1.ownLookup)(table.columnMap, key) ?? key;
|
|
875
|
-
if (isRedactedColumn(table, column, showPii))
|
|
876
|
-
refuse(table, column);
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
|
-
/**
|
|
880
|
-
* A relation predicate arrives in one of two shapes, and BOTH resolve against
|
|
881
|
-
* the relation's target table: bare (`{ user: { email: {...} } }`) or wrapped
|
|
882
|
-
* in a cardinality / to-one operator (`{ user: { is: { email: {...} } } }`,
|
|
883
|
-
* likewise `some` / `none` / `every` / `isNot`). Handing the wrapper straight
|
|
884
|
-
* to `visitClause` walked its keys as if `is` were a column of the target, so
|
|
885
|
-
* the inner clause was never visited and a PII predicate slipped through.
|
|
886
|
-
* Descend into every wrapper member AND into the value itself.
|
|
887
|
-
*/
|
|
888
|
-
const visitRelationValue = (value, target, depth) => {
|
|
889
|
-
assertWithinDepth(depth);
|
|
890
|
-
if (!target || value === null || typeof value !== 'object')
|
|
891
|
-
return;
|
|
892
|
-
const node = value;
|
|
893
|
-
for (const wrapper of RELATION_FILTER_WRAPPERS) {
|
|
894
|
-
if (Object.hasOwn(node, wrapper))
|
|
895
|
-
visitClause(node[wrapper], target, depth + 1);
|
|
896
|
-
}
|
|
897
|
-
visitClause(node, target, depth);
|
|
898
|
-
};
|
|
899
|
-
/** Field-name lists (`distinct`) name columns directly rather than in a clause. */
|
|
900
|
-
const visitFieldList = (value, table) => {
|
|
901
|
-
if (!Array.isArray(value))
|
|
902
|
-
return;
|
|
903
|
-
for (const field of value) {
|
|
904
|
-
if (typeof field !== 'string')
|
|
905
|
-
continue;
|
|
906
|
-
const column = (0, utils_js_1.ownLookup)(table.columnMap, field) ?? field;
|
|
907
|
-
if (isRedactedColumn(table, column, showPii))
|
|
908
|
-
refuse(table, column);
|
|
909
|
-
}
|
|
910
|
-
};
|
|
911
|
-
const visitLevel = (level, table, depth) => {
|
|
912
|
-
assertWithinDepth(depth);
|
|
913
|
-
if (!table)
|
|
914
|
-
return;
|
|
915
|
-
visitClause(level.where, table, depth);
|
|
916
|
-
visitClause(level.orderBy, table, depth);
|
|
917
|
-
// `cursor` is a flat `{ field: value }` seek key that the builder turns into
|
|
918
|
-
// a WHERE range comparison against the sort key, so it reads exactly like a
|
|
919
|
-
// where on the same column.
|
|
920
|
-
visitClause(level.cursor, table, depth);
|
|
921
|
-
visitFieldList(level.distinct, table);
|
|
922
|
-
const withClause = level.with;
|
|
923
|
-
if (!withClause || typeof withClause !== 'object')
|
|
924
|
-
return;
|
|
925
|
-
for (const [relName, spec] of Object.entries(withClause)) {
|
|
926
|
-
const relation = Object.hasOwn(table.relations, relName) ? table.relations[relName] : undefined;
|
|
927
|
-
if (!relation || spec === true || spec === null || typeof spec !== 'object')
|
|
928
|
-
continue;
|
|
929
|
-
visitLevel(spec, metadata.tables[relation.to], depth + 1);
|
|
930
|
-
}
|
|
931
|
-
};
|
|
932
|
-
visitLevel(args, metadata.tables[tableName], 0);
|
|
831
|
+
(0, pii_predicate_guard_js_1.assertNoPiiPredicates)(args, (0, utils_js_1.ownLookup)(metadata.tables, tableName), {
|
|
832
|
+
metadata,
|
|
833
|
+
hiddenReason: (table, column) => (isRedactedColumn(table, column, showPii) ? 'is PII-tagged and redacted' : null),
|
|
834
|
+
refuseColumn: (table, column, reason) => {
|
|
835
|
+
throw new errors_js_1.ValidationError(`[turbine] Column "${column}" on "${table.name}" ${reason}, so it cannot be used ` +
|
|
836
|
+
`in a where, orderBy, cursor, or distinct: filtering, sorting, paging, or de-duplicating on a hidden ` +
|
|
837
|
+
`value reveals it. Restart Studio with --show-pii to query it.`);
|
|
838
|
+
},
|
|
839
|
+
refuseDepth: (maxDepth) => {
|
|
840
|
+
throw new errors_js_1.ValidationError(`[turbine] Query is nested more than ${maxDepth} levels deep, which is past the point where ` +
|
|
841
|
+
`Studio can prove it does not filter or sort on a PII-tagged and redacted column, so it is refused. ` +
|
|
842
|
+
`Flatten the query, or restart Studio with --show-pii.`);
|
|
843
|
+
},
|
|
844
|
+
refuseShape: (table, key) => {
|
|
845
|
+
throw new errors_js_1.ValidationError(`[turbine] Studio does not recognize "${key}" in a query on "${table.name}", so it cannot prove the ` +
|
|
846
|
+
`query does not filter or sort on a PII-tagged and redacted column, and refuses it rather than ` +
|
|
847
|
+
`guessing. Remove it, or restart Studio with --show-pii.`);
|
|
848
|
+
},
|
|
849
|
+
});
|
|
933
850
|
}
|
|
934
851
|
async function apiBuilder(req, res, ctx) {
|
|
935
852
|
const body = await readJsonBody(req);
|
|
@@ -960,7 +877,7 @@ async function apiBuilder(req, res, ctx) {
|
|
|
960
877
|
// here (the user named the columns, and the values are redacted on the way
|
|
961
878
|
// out); only the empty default projection is refused, with a reason instead
|
|
962
879
|
// of a syntax error.
|
|
963
|
-
const target = ctx.metadata.tables
|
|
880
|
+
const target = (0, utils_js_1.ownLookup)(ctx.metadata.tables, tableName);
|
|
964
881
|
if (target && !ctx.showPii && args.select === undefined && target.columns.every((c) => c.pii === true)) {
|
|
965
882
|
sendJson(res, 400, {
|
|
966
883
|
error: `Every column of "${tableName}" is PII-tagged, so the default projection is empty. Name the columns you ` +
|
|
@@ -994,7 +911,17 @@ async function apiBuilder(req, res, ctx) {
|
|
|
994
911
|
// the connection's search_path. Pin it to the configured --schema so the
|
|
995
912
|
// Query tab reads the same schema as the Data tab (set_config is
|
|
996
913
|
// transaction-local and fully parameterized). Demo has no schemas.
|
|
997
|
-
|
|
914
|
+
//
|
|
915
|
+
// The VALUE is bound as a parameter, but Postgres parses the contents of
|
|
916
|
+
// search_path as an identifier LIST, so a name that needs quoting has to
|
|
917
|
+
// carry its own. Passing `My.Schema` raw makes Postgres read it as a
|
|
918
|
+
// two-part token that matches nothing: `current_schemas(false)` came back
|
|
919
|
+
// `{}` and the very next query died with `relation "widgets" does not
|
|
920
|
+
// exist` (measured on PG 16). A schema whose name merely differs in CASE
|
|
921
|
+
// is worse than that, because it silently folds to a DIFFERENT existing
|
|
922
|
+
// schema instead of failing. quoteIdent applies exactly the doubling rule
|
|
923
|
+
// the GUC parser expects.
|
|
924
|
+
await client.query(`SELECT set_config('search_path', $1, true)`, [(0, index_js_1.quoteIdent)(ctx.options.schema)]);
|
|
998
925
|
}
|
|
999
926
|
const started = Date.now();
|
|
1000
927
|
const result = await client.query(deferred.sql, deferred.params);
|
|
@@ -1193,7 +1120,11 @@ async function apiRowWrite(req, res, ctx, op) {
|
|
|
1193
1120
|
await client.query('BEGIN');
|
|
1194
1121
|
if (!ctx.demo) {
|
|
1195
1122
|
await client.query(ctx.statementTimeout.sql, ctx.statementTimeout.params);
|
|
1196
|
-
|
|
1123
|
+
// Quoted for the same reason as the read path above: the bound value is
|
|
1124
|
+
// parsed as an identifier list, so an unquoted mixed-case or dotted schema
|
|
1125
|
+
// name pins search_path to nothing. On THIS path that would mean a write
|
|
1126
|
+
// aimed at a non-default schema resolving somewhere else entirely.
|
|
1127
|
+
await client.query(`SELECT set_config('search_path', $1, true)`, [(0, index_js_1.quoteIdent)(ctx.options.schema)]);
|
|
1197
1128
|
}
|
|
1198
1129
|
const returnedRows = [];
|
|
1199
1130
|
let rowCount = 0;
|
|
@@ -1343,7 +1274,7 @@ async function apiCreateSavedQuery(req, res, ctx) {
|
|
|
1343
1274
|
const body = await readJsonBody(req);
|
|
1344
1275
|
const table = typeof body?.table === 'string' ? body.table : '';
|
|
1345
1276
|
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
|
1346
|
-
if (!table || !ctx.metadata.tables
|
|
1277
|
+
if (!table || !(0, utils_js_1.ownLookup)(ctx.metadata.tables, table)) {
|
|
1347
1278
|
sendJson(res, 400, { error: unknownTableMessage(table, ctx) });
|
|
1348
1279
|
return;
|
|
1349
1280
|
}
|
|
@@ -1452,7 +1383,7 @@ function redactFlatRow(row, piiKeys) {
|
|
|
1452
1383
|
* arrive as parsed json objects keyed by camelCase field names).
|
|
1453
1384
|
*/
|
|
1454
1385
|
function redactBuilderRows(rows, tableName, withClause, metadata) {
|
|
1455
|
-
const table = metadata.tables
|
|
1386
|
+
const table = (0, utils_js_1.ownLookup)(metadata.tables, tableName);
|
|
1456
1387
|
if (!table)
|
|
1457
1388
|
return rows;
|
|
1458
1389
|
const piiKeys = piiKeysForTable(table);
|
package/dist/cjs/cli/ui.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare const symbols: {
|
|
|
33
33
|
readonly info: "i" | "ℹ";
|
|
34
34
|
readonly warning: "⚠" | "!";
|
|
35
35
|
readonly dot: "." | "∙";
|
|
36
|
-
readonly line: "
|
|
36
|
+
readonly line: "-" | "─";
|
|
37
37
|
readonly vertLine: "|" | "│";
|
|
38
38
|
readonly topLeft: "+" | "╭";
|
|
39
39
|
readonly topRight: "+" | "╮";
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -724,6 +724,15 @@ export declare class TurbineClient {
|
|
|
724
724
|
private readonly queryListeners;
|
|
725
725
|
private queryOptions;
|
|
726
726
|
private readonly errorMessagesSafe;
|
|
727
|
+
/**
|
|
728
|
+
* THIS client's error-message mode. Not the process default: see
|
|
729
|
+
* {@link registerClientErrorMessageMode}. Used to scope every operation
|
|
730
|
+
* issued through this client, but only once two clients in the process have
|
|
731
|
+
* asked for different modes.
|
|
732
|
+
*/
|
|
733
|
+
private readonly errorMessageMode;
|
|
734
|
+
/** Per-table mode-scoping accessors, built lazily (see {@link table}). */
|
|
735
|
+
private readonly errorScopedTableCache;
|
|
727
736
|
/** Whether `$on('query')` events carry real params (see `logQueryParams`). */
|
|
728
737
|
private readonly queryParamsVisible;
|
|
729
738
|
/** True when Turbine created the pool and is responsible for tearing it down */
|
|
@@ -913,6 +922,39 @@ export declare class TurbineClient {
|
|
|
913
922
|
* With no replicas the original single-pool instance is returned directly.
|
|
914
923
|
*/
|
|
915
924
|
table<T extends object = Record<string, unknown>>(name: string): QueryInterface<T>;
|
|
925
|
+
/** Get (and cache) the replica-routing accessor for a table. */
|
|
926
|
+
private routingAccessor;
|
|
927
|
+
/**
|
|
928
|
+
* Wrap a table accessor so every operation called through it runs with THIS
|
|
929
|
+
* client's `errorMessages` mode in force, whatever another client set as the
|
|
930
|
+
* process default.
|
|
931
|
+
*
|
|
932
|
+
* A proxy rather than a wrapper object: the QueryInterface mutates its own
|
|
933
|
+
* instance state during a query, so methods must run with the real instance
|
|
934
|
+
* as `this` (the same reason {@link createRoutingAccessor} is built this
|
|
935
|
+
* way). Method wrappers are memoized per property so a hot loop allocates one
|
|
936
|
+
* closure per operation NAME, not per call.
|
|
937
|
+
*
|
|
938
|
+
* The divergence test lives in the wrapper body, evaluated when the operation
|
|
939
|
+
* is CALLED. That placement is the point: an accessor minted before a second
|
|
940
|
+
* client diverges still scopes correctly once it does, and while no client has
|
|
941
|
+
* diverged the module default is already this client's mode, so the wrapper
|
|
942
|
+
* calls straight through and no AsyncLocalStorage scope is established at all.
|
|
943
|
+
*/
|
|
944
|
+
private createErrorModeAccessor;
|
|
945
|
+
/**
|
|
946
|
+
* Run `fn` with this client's `errorMessages` mode in force, or directly when
|
|
947
|
+
* no other client in the process has diverged from it (the common case, which
|
|
948
|
+
* costs nothing). Used by the entry points that do not go through
|
|
949
|
+
* {@link table}: transactions, raw SQL, `pipeline`, and the typed-SQL builder.
|
|
950
|
+
*
|
|
951
|
+
* The typed-SQL builder is the one that cannot be covered by wrapping the
|
|
952
|
+
* call, because it is lazy: `sql` hands this function to `TypedSqlQuery` so
|
|
953
|
+
* the scope reaches the EXECUTION rather than the template. This docstring
|
|
954
|
+
* listed it as covered before 0.66 and it was not, which is the reason the
|
|
955
|
+
* mechanism is spelled out here instead of just named.
|
|
956
|
+
*/
|
|
957
|
+
private withErrorMode;
|
|
916
958
|
/** Get (and cache) the primary-pool-bound QueryInterface for a table. */
|
|
917
959
|
private primaryTableQI;
|
|
918
960
|
/**
|
|
@@ -1065,6 +1107,7 @@ export declare class TurbineClient {
|
|
|
1065
1107
|
* ```
|
|
1066
1108
|
*/
|
|
1067
1109
|
$transaction<T extends readonly DeferredQuery<unknown>[]>(queries: readonly [...T]): Promise<PipelineResults<T>>;
|
|
1110
|
+
private runTransaction;
|
|
1068
1111
|
/**
|
|
1069
1112
|
* Execute a batch of {@link DeferredQuery} objects atomically inside one
|
|
1070
1113
|
* transaction. Backs the `$transaction([...])` array overload. Reuses the raw
|
package/dist/cjs/client.js
CHANGED
|
@@ -648,6 +648,15 @@ class TurbineClient {
|
|
|
648
648
|
queryListeners = new Set();
|
|
649
649
|
queryOptions;
|
|
650
650
|
errorMessagesSafe;
|
|
651
|
+
/**
|
|
652
|
+
* THIS client's error-message mode. Not the process default: see
|
|
653
|
+
* {@link registerClientErrorMessageMode}. Used to scope every operation
|
|
654
|
+
* issued through this client, but only once two clients in the process have
|
|
655
|
+
* asked for different modes.
|
|
656
|
+
*/
|
|
657
|
+
errorMessageMode;
|
|
658
|
+
/** Per-table mode-scoping accessors, built lazily (see {@link table}). */
|
|
659
|
+
errorScopedTableCache = new Map();
|
|
651
660
|
/** Whether `$on('query')` events carry real params (see `logQueryParams`). */
|
|
652
661
|
queryParamsVisible;
|
|
653
662
|
/** True when Turbine created the pool and is responsible for tearing it down */
|
|
@@ -685,6 +694,7 @@ class TurbineClient {
|
|
|
685
694
|
this.logging = parent.logging;
|
|
686
695
|
this.dialect = parent.dialect;
|
|
687
696
|
this.errorMessagesSafe = parent.errorMessagesSafe;
|
|
697
|
+
this.errorMessageMode = parent.errorMessageMode;
|
|
688
698
|
this.queryParamsVisible = parent.queryParamsVisible;
|
|
689
699
|
this.queryOptions = parent.queryOptions;
|
|
690
700
|
this.middlewares = parent.middlewares; // shared reference: $use on parent flows through
|
|
@@ -838,7 +848,8 @@ class TurbineClient {
|
|
|
838
848
|
this.schema = schema;
|
|
839
849
|
// Respect env var kill switch
|
|
840
850
|
const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
|
|
841
|
-
this.
|
|
851
|
+
this.errorMessageMode = config.errorMessages ?? 'safe';
|
|
852
|
+
this.errorMessagesSafe = this.errorMessageMode === 'safe';
|
|
842
853
|
// Query-event param visibility. One derived boolean, so the two config
|
|
843
854
|
// spellings can never disagree: `logQueryParams` wins when set, otherwise
|
|
844
855
|
// `errorMessages` keeps deciding exactly as it always has.
|
|
@@ -886,6 +897,16 @@ class TurbineClient {
|
|
|
886
897
|
};
|
|
887
898
|
// Apply NotFoundError message redaction mode (default: safe, values are
|
|
888
899
|
// stripped from messages to avoid leaking PII into error logs).
|
|
900
|
+
//
|
|
901
|
+
// The process-wide default keeps being set here, unchanged, so a directly
|
|
902
|
+
// constructed error and any single-client process behave exactly as before.
|
|
903
|
+
// What is new is the REGISTRATION: a second client asking for a different
|
|
904
|
+
// mode makes both of them scope their own operations, instead of the last
|
|
905
|
+
// constructor silently deciding for everyone (see
|
|
906
|
+
// registerClientErrorMessageMode). Registration uses the client's effective
|
|
907
|
+
// mode, defaulted, because two clients only agree if their EFFECTIVE modes
|
|
908
|
+
// agree, and an omitted `errorMessages` is an effective 'safe'.
|
|
909
|
+
(0, errors_js_1.registerClientErrorMessageMode)(this.errorMessageMode);
|
|
889
910
|
if (config.errorMessages) {
|
|
890
911
|
(0, errors_js_1.setErrorMessageMode)(config.errorMessages);
|
|
891
912
|
}
|
|
@@ -1227,6 +1248,11 @@ class TurbineClient {
|
|
|
1227
1248
|
this.routingProxyCache.clear();
|
|
1228
1249
|
for (const cache of this.replicaTableCaches)
|
|
1229
1250
|
cache.clear();
|
|
1251
|
+
// The errorMessages accessors too: each proxy holds its target by closure,
|
|
1252
|
+
// so a stale one keeps handing back the pre-$use QueryInterface. Every
|
|
1253
|
+
// cache `table()` reads through has to be listed here, and this one is the
|
|
1254
|
+
// OUTERMOST, so leaving it out silently defeats all three above.
|
|
1255
|
+
this.errorScopedTableCache.clear();
|
|
1230
1256
|
}
|
|
1231
1257
|
// -------------------------------------------------------------------------
|
|
1232
1258
|
// Event emitter, subscribe to query lifecycle events
|
|
@@ -1274,9 +1300,31 @@ class TurbineClient {
|
|
|
1274
1300
|
* With no replicas the original single-pool instance is returned directly.
|
|
1275
1301
|
*/
|
|
1276
1302
|
table(name) {
|
|
1277
|
-
|
|
1278
|
-
|
|
1303
|
+
const base = this.replicaPools.length === 0 ? this.primaryTableQI(name) : this.routingAccessor(name);
|
|
1304
|
+
// The errorMessages accessor is built ALWAYS, not only once two clients have
|
|
1305
|
+
// diverged. It used to be conditional here, and the condition was wrong in
|
|
1306
|
+
// the one direction that leaks: divergence is a property of the PROCESS over
|
|
1307
|
+
// time, while a table accessor is handed out once and kept. An application
|
|
1308
|
+
// that does `const users = db.users` at module load, then constructs a
|
|
1309
|
+
// second client with `errorMessages: 'verbose'` later (an analytics client,
|
|
1310
|
+
// a per-suite test harness), held a bare unscoped reference forever, so the
|
|
1311
|
+
// safe client's `findUniqueOrThrow` misses started rendering row values.
|
|
1312
|
+
// Building the proxy unconditionally is what makes the mode a property of
|
|
1313
|
+
// the client rather than of the construction order.
|
|
1314
|
+
//
|
|
1315
|
+
// The divergence check did not disappear, it moved INSIDE the wrapper, so it
|
|
1316
|
+
// is asked when the operation runs rather than when the accessor is minted.
|
|
1317
|
+
// While every client agrees the wrapper still establishes no scope and the
|
|
1318
|
+
// query path still pays nothing but one proxy trap.
|
|
1319
|
+
let scoped = this.errorScopedTableCache.get(name);
|
|
1320
|
+
if (!scoped) {
|
|
1321
|
+
scoped = this.createErrorModeAccessor(base);
|
|
1322
|
+
this.errorScopedTableCache.set(name, scoped);
|
|
1279
1323
|
}
|
|
1324
|
+
return scoped;
|
|
1325
|
+
}
|
|
1326
|
+
/** Get (and cache) the replica-routing accessor for a table. */
|
|
1327
|
+
routingAccessor(name) {
|
|
1280
1328
|
let proxy = this.routingProxyCache.get(name);
|
|
1281
1329
|
if (!proxy) {
|
|
1282
1330
|
proxy = this.createRoutingAccessor(name);
|
|
@@ -1284,6 +1332,58 @@ class TurbineClient {
|
|
|
1284
1332
|
}
|
|
1285
1333
|
return proxy;
|
|
1286
1334
|
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Wrap a table accessor so every operation called through it runs with THIS
|
|
1337
|
+
* client's `errorMessages` mode in force, whatever another client set as the
|
|
1338
|
+
* process default.
|
|
1339
|
+
*
|
|
1340
|
+
* A proxy rather than a wrapper object: the QueryInterface mutates its own
|
|
1341
|
+
* instance state during a query, so methods must run with the real instance
|
|
1342
|
+
* as `this` (the same reason {@link createRoutingAccessor} is built this
|
|
1343
|
+
* way). Method wrappers are memoized per property so a hot loop allocates one
|
|
1344
|
+
* closure per operation NAME, not per call.
|
|
1345
|
+
*
|
|
1346
|
+
* The divergence test lives in the wrapper body, evaluated when the operation
|
|
1347
|
+
* is CALLED. That placement is the point: an accessor minted before a second
|
|
1348
|
+
* client diverges still scopes correctly once it does, and while no client has
|
|
1349
|
+
* diverged the module default is already this client's mode, so the wrapper
|
|
1350
|
+
* calls straight through and no AsyncLocalStorage scope is established at all.
|
|
1351
|
+
*/
|
|
1352
|
+
createErrorModeAccessor(target) {
|
|
1353
|
+
const mode = this.errorMessageMode;
|
|
1354
|
+
const wrapped = new Map();
|
|
1355
|
+
return new Proxy(target, {
|
|
1356
|
+
get(t, prop, receiver) {
|
|
1357
|
+
const value = Reflect.get(t, prop, receiver);
|
|
1358
|
+
if (typeof value !== 'function' || typeof prop !== 'string')
|
|
1359
|
+
return value;
|
|
1360
|
+
let fn = wrapped.get(prop);
|
|
1361
|
+
if (!fn) {
|
|
1362
|
+
fn = (...args) => {
|
|
1363
|
+
const call = () => value.apply(t, args);
|
|
1364
|
+
return (0, errors_js_1.errorMessageModesDiverged)() ? (0, errors_js_1.runWithErrorMessageMode)(mode, call) : call();
|
|
1365
|
+
};
|
|
1366
|
+
wrapped.set(prop, fn);
|
|
1367
|
+
}
|
|
1368
|
+
return fn;
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Run `fn` with this client's `errorMessages` mode in force, or directly when
|
|
1374
|
+
* no other client in the process has diverged from it (the common case, which
|
|
1375
|
+
* costs nothing). Used by the entry points that do not go through
|
|
1376
|
+
* {@link table}: transactions, raw SQL, `pipeline`, and the typed-SQL builder.
|
|
1377
|
+
*
|
|
1378
|
+
* The typed-SQL builder is the one that cannot be covered by wrapping the
|
|
1379
|
+
* call, because it is lazy: `sql` hands this function to `TypedSqlQuery` so
|
|
1380
|
+
* the scope reaches the EXECUTION rather than the template. This docstring
|
|
1381
|
+
* listed it as covered before 0.66 and it was not, which is the reason the
|
|
1382
|
+
* mechanism is spelled out here instead of just named.
|
|
1383
|
+
*/
|
|
1384
|
+
withErrorMode(fn) {
|
|
1385
|
+
return (0, errors_js_1.errorMessageModesDiverged)() ? (0, errors_js_1.runWithErrorMessageMode)(this.errorMessageMode, fn) : fn();
|
|
1386
|
+
}
|
|
1287
1387
|
/** Get (and cache) the primary-pool-bound QueryInterface for a table. */
|
|
1288
1388
|
primaryTableQI(name) {
|
|
1289
1389
|
let qi = this.tableCache.get(name);
|
|
@@ -1401,7 +1501,14 @@ class TurbineClient {
|
|
|
1401
1501
|
if (this.logging) {
|
|
1402
1502
|
console.log(`[turbine] Pipeline: ${queries.length} queries, ${queries.map((q) => q.tag).join(', ')}`);
|
|
1403
1503
|
}
|
|
1404
|
-
|
|
1504
|
+
// Scoped like every other entry point. A pipeline builds NotFoundErrors (a
|
|
1505
|
+
// `buildFindUniqueOrThrow` slot) and wraps driver errors into
|
|
1506
|
+
// `PipelineError.results[i]`, and none of that went through a mode scope
|
|
1507
|
+
// before, so a safe client's miss inside a pipeline rendered the caller's
|
|
1508
|
+
// where VALUES as soon as any verbose client existed in the process. The
|
|
1509
|
+
// scope is established around the await, so every continuation of the batch
|
|
1510
|
+
// resolves this client's mode.
|
|
1511
|
+
return this.withErrorMode(() => (0, pipeline_js_1.executePipeline)(this.pool, queries, options));
|
|
1405
1512
|
}
|
|
1406
1513
|
/**
|
|
1407
1514
|
* Check whether the underlying pool supports the real pipeline protocol.
|
|
@@ -1442,7 +1549,7 @@ class TurbineClient {
|
|
|
1442
1549
|
return result.rows;
|
|
1443
1550
|
}
|
|
1444
1551
|
catch (err) {
|
|
1445
|
-
throw (0, errors_js_1.wrapPgError)(err);
|
|
1552
|
+
throw this.withErrorMode(() => (0, errors_js_1.wrapPgError)(err));
|
|
1446
1553
|
}
|
|
1447
1554
|
}
|
|
1448
1555
|
/**
|
|
@@ -1477,7 +1584,13 @@ class TurbineClient {
|
|
|
1477
1584
|
*/
|
|
1478
1585
|
sql(strings, ...values) {
|
|
1479
1586
|
const { sql, params } = (0, typed_sql_js_1.buildTypedSql)(strings, values, this.dialect);
|
|
1480
|
-
|
|
1587
|
+
// The scope has to be handed to the BUILDER, not wrapped around this call.
|
|
1588
|
+
// `TypedSqlQuery` is lazy (it runs on `await` / `.one()` / `.scalar()`), so
|
|
1589
|
+
// a scope opened here would close before any row was fetched. The
|
|
1590
|
+
// `withErrorMode` docstring already claimed to cover the typed-SQL builder
|
|
1591
|
+
// and did not, which left two raw-SQL entry points disagreeing about the
|
|
1592
|
+
// same statement, since the adjacent `raw` tag WAS scoped.
|
|
1593
|
+
return new typed_sql_js_1.TypedSqlQuery(this.pool, sql, params, this.logging, (fn) => this.withErrorMode(fn));
|
|
1481
1594
|
}
|
|
1482
1595
|
// -------------------------------------------------------------------------
|
|
1483
1596
|
// Transaction support (raw, legacy)
|
|
@@ -1532,6 +1645,12 @@ class TurbineClient {
|
|
|
1532
1645
|
}
|
|
1533
1646
|
}
|
|
1534
1647
|
async $transaction(fnOrQueries, options) {
|
|
1648
|
+
// Scope the WHOLE call, so the transaction-scoped QueryInterfaces built
|
|
1649
|
+
// inside TransactionClient.table() inherit this client's mode without
|
|
1650
|
+
// needing their own accessor.
|
|
1651
|
+
return this.withErrorMode(() => this.runTransaction(fnOrQueries, options));
|
|
1652
|
+
}
|
|
1653
|
+
async runTransaction(fnOrQueries, options) {
|
|
1535
1654
|
// Batch overload: an array of DeferredQuery objects runs atomically inside
|
|
1536
1655
|
// one BEGIN…COMMIT, reusing the raw transaction machinery below.
|
|
1537
1656
|
if (Array.isArray(fnOrQueries)) {
|