turbine-orm 0.52.0 → 0.53.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/dist/cjs/client.js +152 -0
- package/dist/cjs/nested-write.js +51 -5
- package/dist/cjs/prisma-compat.d.ts +80 -2
- package/dist/cjs/prisma-compat.js +249 -74
- package/dist/cjs/query/aggregates.d.ts +33 -9
- package/dist/cjs/query/aggregates.js +180 -40
- package/dist/cjs/query/builder.d.ts +23 -2
- package/dist/cjs/query/builder.js +72 -45
- package/dist/cjs/query/types.d.ts +49 -24
- package/dist/cjs/query/utils.d.ts +30 -0
- package/dist/cjs/query/utils.js +35 -0
- package/dist/cjs/query/warn-registry.d.ts +7 -0
- package/dist/cjs/query/warn-registry.js +7 -0
- package/dist/cjs/query/writes.d.ts +5 -1
- package/dist/cjs/query/writes.js +31 -6
- package/dist/client.js +153 -1
- package/dist/nested-write.js +51 -5
- package/dist/prisma-compat.d.ts +80 -2
- package/dist/prisma-compat.js +249 -74
- package/dist/query/aggregates.d.ts +33 -9
- package/dist/query/aggregates.js +181 -41
- package/dist/query/builder.d.ts +23 -2
- package/dist/query/builder.js +74 -47
- package/dist/query/types.d.ts +49 -24
- package/dist/query/utils.d.ts +30 -0
- package/dist/query/utils.js +35 -1
- package/dist/query/warn-registry.d.ts +7 -0
- package/dist/query/warn-registry.js +7 -0
- package/dist/query/writes.d.ts +5 -1
- package/dist/query/writes.js +32 -7
- package/package.json +1 -1
|
@@ -23,6 +23,36 @@ export declare function quoteIdent(name: string): string;
|
|
|
23
23
|
* unless `key` is an OWN enumerable/non-enumerable property.
|
|
24
24
|
*/
|
|
25
25
|
export declare function ownLookup<T>(map: Record<string, T>, key: string): T | undefined;
|
|
26
|
+
/** The metadata a key needs to be resolved to a column. `TableMetadata` fits. */
|
|
27
|
+
export interface ColumnNameSource {
|
|
28
|
+
columnMap: Record<string, string>;
|
|
29
|
+
reverseColumnMap?: Record<string, string>;
|
|
30
|
+
allColumns?: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a user-supplied key to its unquoted column name, or `undefined` when
|
|
34
|
+
* the key names no column on the table.
|
|
35
|
+
*
|
|
36
|
+
* THE key-resolution rule, in one place. `QueryInterface.toColumn` is this
|
|
37
|
+
* function plus the E003 throw, so every SQL builder resolves keys through it,
|
|
38
|
+
* and the value-side passes (write coercion, the `updatedAt` injector, the
|
|
39
|
+
* nested-write foreign-key merge) call it directly rather than re-deriving the
|
|
40
|
+
* rule. They used to read `columnMap` alone, which knows only the FIELD
|
|
41
|
+
* spelling, so a key spelled as the snake_case COLUMN, which the SQL builders
|
|
42
|
+
* accept and which is the natural spelling on an introspected schema, produced
|
|
43
|
+
* correct SQL with an unprocessed value: byte-identical statement, silently
|
|
44
|
+
* different bound param.
|
|
45
|
+
*
|
|
46
|
+
* The rule: the field map first, else `camelToSnake(key)` accepted ONLY when
|
|
47
|
+
* that name is a real column. `camelToSnake` is idempotent on an already-snake
|
|
48
|
+
* string, which is what makes the column spelling legal; arbitrary strings
|
|
49
|
+
* still fail to resolve, so identifier validation is unchanged.
|
|
50
|
+
*
|
|
51
|
+
* Prototype-safe: both maps are plain objects, so a key like "constructor" or
|
|
52
|
+
* "__proto__" would otherwise return an inherited member and pass for a column
|
|
53
|
+
* name (see {@link ownLookup}).
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveColumnName(meta: ColumnNameSource, key: string): string | undefined;
|
|
26
56
|
/**
|
|
27
57
|
* Escape single quotes for use as string keys in json_build_object().
|
|
28
58
|
* Doubles single quotes per SQL quoting rules.
|
package/dist/cjs/query/utils.js
CHANGED
|
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
11
11
|
exports.JSON_WIRE_COERCION_OIDS = exports.OPERATOR_KEYS = exports.LRUCache = void 0;
|
|
12
12
|
exports.quoteIdent = quoteIdent;
|
|
13
13
|
exports.ownLookup = ownLookup;
|
|
14
|
+
exports.resolveColumnName = resolveColumnName;
|
|
14
15
|
exports.escSingleQuote = escSingleQuote;
|
|
15
16
|
exports.escapeLike = escapeLike;
|
|
16
17
|
exports.fnv1a64Hex = fnv1a64Hex;
|
|
@@ -54,6 +55,40 @@ function quoteIdent(name) {
|
|
|
54
55
|
function ownLookup(map, key) {
|
|
55
56
|
return Object.hasOwn(map, key) ? map[key] : undefined;
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a user-supplied key to its unquoted column name, or `undefined` when
|
|
60
|
+
* the key names no column on the table.
|
|
61
|
+
*
|
|
62
|
+
* THE key-resolution rule, in one place. `QueryInterface.toColumn` is this
|
|
63
|
+
* function plus the E003 throw, so every SQL builder resolves keys through it,
|
|
64
|
+
* and the value-side passes (write coercion, the `updatedAt` injector, the
|
|
65
|
+
* nested-write foreign-key merge) call it directly rather than re-deriving the
|
|
66
|
+
* rule. They used to read `columnMap` alone, which knows only the FIELD
|
|
67
|
+
* spelling, so a key spelled as the snake_case COLUMN, which the SQL builders
|
|
68
|
+
* accept and which is the natural spelling on an introspected schema, produced
|
|
69
|
+
* correct SQL with an unprocessed value: byte-identical statement, silently
|
|
70
|
+
* different bound param.
|
|
71
|
+
*
|
|
72
|
+
* The rule: the field map first, else `camelToSnake(key)` accepted ONLY when
|
|
73
|
+
* that name is a real column. `camelToSnake` is idempotent on an already-snake
|
|
74
|
+
* string, which is what makes the column spelling legal; arbitrary strings
|
|
75
|
+
* still fail to resolve, so identifier validation is unchanged.
|
|
76
|
+
*
|
|
77
|
+
* Prototype-safe: both maps are plain objects, so a key like "constructor" or
|
|
78
|
+
* "__proto__" would otherwise return an inherited member and pass for a column
|
|
79
|
+
* name (see {@link ownLookup}).
|
|
80
|
+
*/
|
|
81
|
+
function resolveColumnName(meta, key) {
|
|
82
|
+
const mapped = ownLookup(meta.columnMap, key);
|
|
83
|
+
if (mapped)
|
|
84
|
+
return mapped;
|
|
85
|
+
const snake = (0, schema_js_1.camelToSnake)(key);
|
|
86
|
+
if (meta.reverseColumnMap && ownLookup(meta.reverseColumnMap, snake))
|
|
87
|
+
return snake;
|
|
88
|
+
if (meta.allColumns?.includes(snake))
|
|
89
|
+
return snake;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
57
92
|
/**
|
|
58
93
|
* Escape single quotes for use as string keys in json_build_object().
|
|
59
94
|
* Doubles single quotes per SQL quoting rules.
|
|
@@ -58,6 +58,13 @@ export declare const WARN_NS: {
|
|
|
58
58
|
readonly unorderedPage: "unorderedPage";
|
|
59
59
|
/** PowDB `emitLinks` DDL skips (name/column collision, endpoint drift). */
|
|
60
60
|
readonly powdbLinks: "powdbLinks";
|
|
61
|
+
/**
|
|
62
|
+
* A key on the object passed as `TurbineConfig` that is not part of the
|
|
63
|
+
* config surface (client.ts `warnUnknownConfigKeys`). Keyed on the unknown
|
|
64
|
+
* key name, so a process that builds many clients from the same misspelled
|
|
65
|
+
* config says it once.
|
|
66
|
+
*/
|
|
67
|
+
readonly unknownConfigKey: "unknownConfigKey";
|
|
61
68
|
/**
|
|
62
69
|
* `relationLoadStrategy: 'flatten'` was asked for but a relation stayed on the
|
|
63
70
|
* correlated-subquery path (relations.ts `planFlattenWith`, builder.ts
|
|
@@ -99,6 +99,13 @@ exports.WARN_NS = {
|
|
|
99
99
|
unorderedPage: 'unorderedPage',
|
|
100
100
|
/** PowDB `emitLinks` DDL skips (name/column collision, endpoint drift). */
|
|
101
101
|
powdbLinks: 'powdbLinks',
|
|
102
|
+
/**
|
|
103
|
+
* A key on the object passed as `TurbineConfig` that is not part of the
|
|
104
|
+
* config surface (client.ts `warnUnknownConfigKeys`). Keyed on the unknown
|
|
105
|
+
* key name, so a process that builds many clients from the same misspelled
|
|
106
|
+
* config says it once.
|
|
107
|
+
*/
|
|
108
|
+
unknownConfigKey: 'unknownConfigKey',
|
|
102
109
|
/**
|
|
103
110
|
* `relationLoadStrategy: 'flatten'` was asked for but a relation stayed on the
|
|
104
111
|
* correlated-subquery path (relations.ts `planFlattenWith`, builder.ts
|
|
@@ -112,7 +112,11 @@ export declare function piiFields(_qi: BuilderCtx, meta: TableMetadata): string[
|
|
|
112
112
|
* `Date` and lands in UTC on every engine.
|
|
113
113
|
*
|
|
114
114
|
* An explicit value always wins, including an explicit `null`: naming the
|
|
115
|
-
* column is a statement of intent.
|
|
115
|
+
* column is a statement of intent. "Named" is decided by resolving each data
|
|
116
|
+
* key to its COLUMN, not by matching the field spelling: the SET list resolves
|
|
117
|
+
* the caller's key the same way, so a key spelled as the snake_case column used
|
|
118
|
+
* to be missed here and the injected field assigned the same column a second
|
|
119
|
+
* time (`SET "updated_at" = $1, "updated_at" = $2`, PostgreSQL 42701).
|
|
116
120
|
*/
|
|
117
121
|
export declare function applyUpdatedAtColumns(qi: BuilderCtx, data: Record<string, unknown>): Record<string, unknown>;
|
|
118
122
|
export declare function writeReturningColumns(qi: BuilderCtx): ReturningSelection;
|
package/dist/cjs/query/writes.js
CHANGED
|
@@ -99,10 +99,14 @@ function coerceWriteValue(qi, key, value) {
|
|
|
99
99
|
// Cheap shape check first: the common path costs one check and no lookup.
|
|
100
100
|
if (!(value instanceof Date) && !Array.isArray(value))
|
|
101
101
|
return value;
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// the
|
|
105
|
-
|
|
102
|
+
// THE key-resolution rule, shared with `toColumn` (the SQL side of this very
|
|
103
|
+
// statement) so the two can never disagree about which column a key names:
|
|
104
|
+
// resolving through `columnMap` alone missed the snake_case COLUMN spelling
|
|
105
|
+
// that the SQL builders accept, so those writes bound an unprocessed value
|
|
106
|
+
// under an identical statement. Non-throwing, because this also runs on the
|
|
107
|
+
// cache-HIT param-collect path, where an unknown key must not turn into a
|
|
108
|
+
// different error than the build path already raises.
|
|
109
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
106
110
|
if (!column)
|
|
107
111
|
return value;
|
|
108
112
|
// Metadata generated by an older Turbine still carries per-column types
|
|
@@ -751,22 +755,43 @@ function piiFields(_qi, meta) {
|
|
|
751
755
|
* `Date` and lands in UTC on every engine.
|
|
752
756
|
*
|
|
753
757
|
* An explicit value always wins, including an explicit `null`: naming the
|
|
754
|
-
* column is a statement of intent.
|
|
758
|
+
* column is a statement of intent. "Named" is decided by resolving each data
|
|
759
|
+
* key to its COLUMN, not by matching the field spelling: the SET list resolves
|
|
760
|
+
* the caller's key the same way, so a key spelled as the snake_case column used
|
|
761
|
+
* to be missed here and the injected field assigned the same column a second
|
|
762
|
+
* time (`SET "updated_at" = $1, "updated_at" = $2`, PostgreSQL 42701).
|
|
755
763
|
*/
|
|
756
764
|
function applyUpdatedAtColumns(qi, data) {
|
|
757
765
|
const tagged = qi.tableMeta.columns.filter((c) => c.updatedAt);
|
|
758
766
|
if (tagged.length === 0)
|
|
759
767
|
return data;
|
|
768
|
+
const named = namedColumns(qi.tableMeta, data);
|
|
760
769
|
let out = null;
|
|
761
770
|
const now = new Date();
|
|
762
771
|
for (const col of tagged) {
|
|
763
|
-
if (
|
|
772
|
+
if (named.has(col.name))
|
|
764
773
|
continue;
|
|
765
774
|
out ??= { ...data };
|
|
766
775
|
out[col.field] = now;
|
|
767
776
|
}
|
|
768
777
|
return out ?? data;
|
|
769
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* The set of COLUMNS a `data` object names, under any accepted spelling of each
|
|
781
|
+
* key. A key set to `undefined` names nothing (see {@link definedKeys}), and a
|
|
782
|
+
* key that resolves to no column is left to the SQL builder's own E003.
|
|
783
|
+
*/
|
|
784
|
+
function namedColumns(meta, data) {
|
|
785
|
+
const out = new Set();
|
|
786
|
+
for (const key of Object.keys(data)) {
|
|
787
|
+
if (data[key] === undefined)
|
|
788
|
+
continue;
|
|
789
|
+
const column = (0, utils_js_1.resolveColumnName)(meta, key);
|
|
790
|
+
if (column)
|
|
791
|
+
out.add(column);
|
|
792
|
+
}
|
|
793
|
+
return out;
|
|
794
|
+
}
|
|
770
795
|
function writeReturningColumns(qi) {
|
|
771
796
|
const piiCols = piiColumns(qi, qi.tableMeta);
|
|
772
797
|
if (piiCols.size === 0)
|
package/dist/client.js
CHANGED
|
@@ -27,7 +27,8 @@ import { setErrorMessageMode, TimeoutError, UnsupportedFeatureError, ValidationE
|
|
|
27
27
|
import { ObserveEngine } from './observe.js';
|
|
28
28
|
import { executePipeline, pipelineSupported } from './pipeline.js';
|
|
29
29
|
import { QueryInterface, } from './query/index.js';
|
|
30
|
-
import { quoteIdent } from './query/utils.js';
|
|
30
|
+
import { closestName, quoteIdent } from './query/utils.js';
|
|
31
|
+
import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
|
|
31
32
|
import { createSubscription, validateChannel, } from './realtime.js';
|
|
32
33
|
import { buildTypedSql, TypedSqlQuery } from './typed-sql.js';
|
|
33
34
|
export async function withRetry(fn, options) {
|
|
@@ -54,6 +55,154 @@ export async function withRetry(fn, options) {
|
|
|
54
55
|
}
|
|
55
56
|
throw lastError;
|
|
56
57
|
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Unknown-config-key diagnostics
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
/**
|
|
62
|
+
* Every `TurbineConfig` key, as runtime data. TypeScript erases the interface,
|
|
63
|
+
* so the key set has to exist as a value; `Record<keyof TurbineConfig, true>`
|
|
64
|
+
* makes the compiler own it in BOTH directions: a field added to the interface
|
|
65
|
+
* fails typecheck until it is listed here, and a key listed here that is not a
|
|
66
|
+
* field fails as an excess property. So this can never drift into warning about
|
|
67
|
+
* a real option.
|
|
68
|
+
*/
|
|
69
|
+
const TURBINE_CONFIG_KEYS = {
|
|
70
|
+
pool: true,
|
|
71
|
+
connectionString: true,
|
|
72
|
+
host: true,
|
|
73
|
+
port: true,
|
|
74
|
+
database: true,
|
|
75
|
+
user: true,
|
|
76
|
+
password: true,
|
|
77
|
+
ssl: true,
|
|
78
|
+
poolSize: true,
|
|
79
|
+
idleTimeoutMs: true,
|
|
80
|
+
connectionTimeoutMs: true,
|
|
81
|
+
max: true,
|
|
82
|
+
idleTimeoutMillis: true,
|
|
83
|
+
connectionTimeoutMillis: true,
|
|
84
|
+
logging: true,
|
|
85
|
+
defaultLimit: true,
|
|
86
|
+
warnOnUnlimited: true,
|
|
87
|
+
utcTimestamps: true,
|
|
88
|
+
scopedConnect: true,
|
|
89
|
+
relationLoadStrategy: true,
|
|
90
|
+
stableRelationOrder: true,
|
|
91
|
+
implicitPkOrdering: true,
|
|
92
|
+
autoToOneJoinMaxRows: true,
|
|
93
|
+
autoRoundTripMs: true,
|
|
94
|
+
jsonEncoding: true,
|
|
95
|
+
errorMessages: true,
|
|
96
|
+
logQueryParams: true,
|
|
97
|
+
preparedStatements: true,
|
|
98
|
+
sqlCache: true,
|
|
99
|
+
sqlCacheSize: true,
|
|
100
|
+
dialect: true,
|
|
101
|
+
replicas: true,
|
|
102
|
+
globalFilters: true,
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* {@link TURBINE_CONFIG_KEYS} as a lookup. A `Set` rather than an `in` test on
|
|
106
|
+
* the record, so an inherited `Object.prototype` name (`toString`, `constructor`)
|
|
107
|
+
* is treated as the unknown key it is.
|
|
108
|
+
*/
|
|
109
|
+
const CONFIG_KEY_SET = new Set(Object.keys(TURBINE_CONFIG_KEYS));
|
|
110
|
+
/**
|
|
111
|
+
* Keys that are legitimately present on a config object but are not public
|
|
112
|
+
* `TurbineConfig` fields:
|
|
113
|
+
*
|
|
114
|
+
* - `queryInterfaceFactory`: the non-SQL-backend seam. `turbinePowDB` sets it
|
|
115
|
+
* through a cast so `table()` builds a `PowqlInterface`; it is `@internal`,
|
|
116
|
+
* deliberately absent from the public interface, and must not warn.
|
|
117
|
+
* - `schema`: `turbine.config.*` files carry a Postgres schema NAME for the
|
|
118
|
+
* CLI, and that same object is routinely spread into the client factory.
|
|
119
|
+
* The client ignores it (its schema metadata is the second argument), and
|
|
120
|
+
* shouting about a documented CLI field would be pure noise.
|
|
121
|
+
* - `url`: the connection-string spelling used by the CLI config and by the
|
|
122
|
+
* engine factories' first argument. Same story as `schema`.
|
|
123
|
+
*/
|
|
124
|
+
const NON_CONFIG_KEYS = new Set(['queryInterfaceFactory', 'schema', 'url']);
|
|
125
|
+
/** camelCase name → its lowercased words (`logQueryParams` → log, query, params). */
|
|
126
|
+
function camelWords(name) {
|
|
127
|
+
return name
|
|
128
|
+
.split(/(?=[A-Z])/)
|
|
129
|
+
.map((w) => w.toLowerCase())
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The real config key `key` most likely meant, or null when nothing is close.
|
|
134
|
+
*
|
|
135
|
+
* {@link closestName} (the same helper the unknown-COLUMN message uses) decides
|
|
136
|
+
* first, so both diagnostics rank near-misses identically. It is bounded by edit
|
|
137
|
+
* distance, which covers typos but not the miss this warning exists for: a
|
|
138
|
+
* guessed name that omits a whole word. `logParams` is five edits from
|
|
139
|
+
* `logQueryParams`, past the bound, yet it names the same words in the same
|
|
140
|
+
* order, so a second pass accepts a candidate whose camelCase words CONTAIN the
|
|
141
|
+
* guess's words in order, preferring the one that adds fewest words.
|
|
142
|
+
*/
|
|
143
|
+
function suggestConfigKey(key) {
|
|
144
|
+
const direct = closestName(key, CONFIG_KEY_SET);
|
|
145
|
+
if (direct)
|
|
146
|
+
return direct;
|
|
147
|
+
const wanted = camelWords(key);
|
|
148
|
+
if (wanted.length < 2)
|
|
149
|
+
return null;
|
|
150
|
+
let best = null;
|
|
151
|
+
let bestExtra = Number.POSITIVE_INFINITY;
|
|
152
|
+
for (const candidate of CONFIG_KEY_SET) {
|
|
153
|
+
const words = camelWords(candidate);
|
|
154
|
+
if (words.length <= wanted.length)
|
|
155
|
+
continue;
|
|
156
|
+
let i = 0;
|
|
157
|
+
for (const w of words)
|
|
158
|
+
if (w === wanted[i])
|
|
159
|
+
i++;
|
|
160
|
+
if (i !== wanted.length)
|
|
161
|
+
continue;
|
|
162
|
+
const extra = words.length - wanted.length;
|
|
163
|
+
if (extra < bestExtra) {
|
|
164
|
+
bestExtra = extra;
|
|
165
|
+
best = candidate;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return best;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Dev-mode notice for a key on the config object that is not part of the config
|
|
172
|
+
* surface.
|
|
173
|
+
*
|
|
174
|
+
* An unknown key is silently ignored (JavaScript objects have no schema), which
|
|
175
|
+
* makes a typo or a wrong guess indistinguishable from a broken feature: a
|
|
176
|
+
* caller who wants query parameters in `$on('query')` events and reaches for a
|
|
177
|
+
* plausible-sounding `logParams` sees nothing happen and concludes the feature
|
|
178
|
+
* does not work, rather than that the option is spelled `logQueryParams`.
|
|
179
|
+
*
|
|
180
|
+
* Deliberately a warning, never an error. An app compiled against a NEWER
|
|
181
|
+
* turbine that passes a key this version has not heard of must keep running,
|
|
182
|
+
* and the whole check is wrapped so that a hostile / exotic config object
|
|
183
|
+
* (a Proxy whose `ownKeys` throws) cannot take down the constructor either.
|
|
184
|
+
* Dev-only (`NODE_ENV !== 'production'`) and once per key per process, like the
|
|
185
|
+
* other advisory diagnostics.
|
|
186
|
+
*/
|
|
187
|
+
function warnUnknownConfigKeys(config) {
|
|
188
|
+
if (process.env.NODE_ENV === 'production')
|
|
189
|
+
return;
|
|
190
|
+
try {
|
|
191
|
+
for (const key of Object.keys(config)) {
|
|
192
|
+
if (CONFIG_KEY_SET.has(key) || NON_CONFIG_KEYS.has(key))
|
|
193
|
+
continue;
|
|
194
|
+
if (!shouldWarnOnce(WARN_NS.unknownConfigKey, key))
|
|
195
|
+
continue;
|
|
196
|
+
const suggestion = suggestConfigKey(key);
|
|
197
|
+
console.warn(`[turbine] Unknown option "${key}" in the config passed to TurbineClient, it is ignored.` +
|
|
198
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : ''));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Key enumeration is the only thing that can fail here, and a diagnostic
|
|
203
|
+
// must never be the reason a client fails to construct.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
57
206
|
/** Maps isolation level names to SQL */
|
|
58
207
|
const ISOLATION_LEVELS = {
|
|
59
208
|
ReadUncommitted: 'READ UNCOMMITTED',
|
|
@@ -378,6 +527,9 @@ export class TurbineClient {
|
|
|
378
527
|
}
|
|
379
528
|
break;
|
|
380
529
|
}
|
|
530
|
+
// Name any key on the config object that is not part of the config surface
|
|
531
|
+
// (dev only, once per key, never throws). See warnUnknownConfigKeys.
|
|
532
|
+
warnUnknownConfigKeys(config);
|
|
381
533
|
/**
|
|
382
534
|
* Parse int8 (bigint, OID 20) as JavaScript number instead of string.
|
|
383
535
|
* Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
|
package/dist/nested-write.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `NestedWriteContext`.
|
|
13
13
|
*/
|
|
14
14
|
import { CircularRelationError, describeTargetForMessage, NotFoundError, RelationError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
15
|
+
import { resolveColumnName } from './query/utils.js';
|
|
15
16
|
import { normalizeKeyColumns } from './schema.js';
|
|
16
17
|
const MAX_DEPTH = 10;
|
|
17
18
|
const CREATE_ONLY_OPS = new Set(['create', 'connect', 'connectOrCreate']);
|
|
@@ -75,10 +76,50 @@ export function injectForeignKey(childData, relation, parentRow, schema) {
|
|
|
75
76
|
const refCol = refs[i];
|
|
76
77
|
const refField = schema.tables[relation.from]?.reverseColumnMap[refCol] ?? refCol;
|
|
77
78
|
const fkField = childTable?.reverseColumnMap[fkCol] ?? fkCol;
|
|
78
|
-
result
|
|
79
|
+
assignByColumn(result, childTable, fkField, parentRow[refField]);
|
|
79
80
|
}
|
|
80
81
|
return result;
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Set `field` on `target`, first dropping any OTHER key that names the SAME
|
|
85
|
+
* column.
|
|
86
|
+
*
|
|
87
|
+
* The engine always writes the canonical FIELD spelling, while a caller's own
|
|
88
|
+
* `data` may legally spell the same column its snake_case way (the write
|
|
89
|
+
* builders resolve both). Overwriting only the identical key left both in the
|
|
90
|
+
* object, and the INSERT/UPDATE then named one column twice, which PostgreSQL
|
|
91
|
+
* refuses (42701 "specified more than once"). Dropping the alias makes the
|
|
92
|
+
* column spelling behave exactly as the field spelling always did: the value the
|
|
93
|
+
* relation dictates wins.
|
|
94
|
+
*
|
|
95
|
+
* Only SCALAR keys are droppable. A relation is resolved to a column by the very
|
|
96
|
+
* same rule (nothing stops a schema from naming a relation the way a column is
|
|
97
|
+
* spelled), but a relation key carries a nested write rather than a value, so
|
|
98
|
+
* dropping it would discard the whole operation silently, a strictly worse
|
|
99
|
+
* outcome than the duplicate-column error this drop exists to prevent. The
|
|
100
|
+
* relation-shape test matches {@link splitData}, so a key routed to `relations`
|
|
101
|
+
* there is never treated as an alias here.
|
|
102
|
+
*/
|
|
103
|
+
function assignByColumn(target, meta, field, value) {
|
|
104
|
+
const column = meta && resolveColumnName(meta, field);
|
|
105
|
+
if (column) {
|
|
106
|
+
for (const key of Object.keys(target)) {
|
|
107
|
+
if (key === field || isRelationEntry(meta, key, target[key]))
|
|
108
|
+
continue;
|
|
109
|
+
if (resolveColumnName(meta, key) === column)
|
|
110
|
+
delete target[key];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
target[field] = value;
|
|
114
|
+
}
|
|
115
|
+
/** Does `key` name a relation on `meta` AND carry a nested-write payload? */
|
|
116
|
+
function isRelationEntry(meta, key, value) {
|
|
117
|
+
return (Object.hasOwn(meta.relations, key) &&
|
|
118
|
+
value !== null &&
|
|
119
|
+
typeof value === 'object' &&
|
|
120
|
+
!Array.isArray(value) &&
|
|
121
|
+
!(value instanceof Date));
|
|
122
|
+
}
|
|
82
123
|
/**
|
|
83
124
|
* Split rows destined for `createMany` into CONTIGUOUS runs that each name the
|
|
84
125
|
* same fields.
|
|
@@ -741,10 +782,15 @@ export async function executeNestedCreate(ctx, tableName, data, depth = 0, path
|
|
|
741
782
|
assertManyToManyOpsSupported(relName, rel, ops);
|
|
742
783
|
}
|
|
743
784
|
}
|
|
744
|
-
// Insert the parent row (scalars + resolved belongsTo foreign keys)
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
785
|
+
// Insert the parent row (scalars + resolved belongsTo foreign keys). The
|
|
786
|
+
// resolved keys win over a caller-supplied value for the same column under
|
|
787
|
+
// either spelling (see assignByColumn), so `{ authorId: 1, author: { connect } }`
|
|
788
|
+
// and `{ author_id: 1, author: { connect } }` both take the connected row.
|
|
789
|
+
const parentData = { ...scalars };
|
|
790
|
+
for (const [field, value] of Object.entries(belongsToFks)) {
|
|
791
|
+
assignByColumn(parentData, tableMeta, field, value);
|
|
792
|
+
}
|
|
793
|
+
const parentRow = (await ctx.tx.table(tableName).create({ data: parentData }));
|
|
748
794
|
// Process hasMany / hasOne relations, their FK lives on the CHILD, so they
|
|
749
795
|
// need the parent row to exist first.
|
|
750
796
|
for (const [relName, ops] of Object.entries(relations)) {
|
package/dist/prisma-compat.d.ts
CHANGED
|
@@ -37,7 +37,15 @@
|
|
|
37
37
|
* These cannot be faithfully translated and are not attempted; each throws or is
|
|
38
38
|
* documented rather than silently returning wrong data:
|
|
39
39
|
*
|
|
40
|
-
* -
|
|
40
|
+
* - **`$extends` beyond `client` + `model`.** Client extensions ARE supported for
|
|
41
|
+
* those two components (plus the `Prisma.defineExtension` callback form), and
|
|
42
|
+
* return a new client whose delegates, `$transaction` and raw surface all
|
|
43
|
+
* survive. The `query` (interception) and `result` (computed fields)
|
|
44
|
+
* components, and any component this adapter does not recognize, throw an
|
|
45
|
+
* {@link UnsupportedFeatureError} naming the component AT `$extends` TIME
|
|
46
|
+
* rather than being accepted and quietly not applied.
|
|
47
|
+
* - `$use` with Prisma's middleware param shape (Turbine's own `client.$use` is
|
|
48
|
+
* the supported interception seam).
|
|
41
49
|
* - `instanceof PrismaClientKnownRequestError`, `.meta`/message byte parity
|
|
42
50
|
* (opt into `prismaErrorCodes` for a `.code` like `P2002`, without pretending
|
|
43
51
|
* `instanceof` identity).
|
|
@@ -197,6 +205,19 @@ export declare const Prisma: {
|
|
|
197
205
|
raw(sql: string): Sql;
|
|
198
206
|
/** An empty fragment. */
|
|
199
207
|
empty: Sql;
|
|
208
|
+
/**
|
|
209
|
+
* The extension context of `this` inside a client / model extension method.
|
|
210
|
+
* Turbine binds extension members directly onto the client and delegate
|
|
211
|
+
* objects, so the context IS `this`; the identity function exists so migrated
|
|
212
|
+
* `Prisma.getExtensionContext(this).$name` call sites keep working.
|
|
213
|
+
*/
|
|
214
|
+
getExtensionContext<T>(that: T): T;
|
|
215
|
+
/**
|
|
216
|
+
* Type-preserving passthrough for `Prisma.defineExtension(ext)`. Prisma uses
|
|
217
|
+
* it purely for inference; the value is returned unchanged, so both the object
|
|
218
|
+
* and the callback form reach `$extends` intact.
|
|
219
|
+
*/
|
|
220
|
+
defineExtension<E>(ext: E): E;
|
|
200
221
|
};
|
|
201
222
|
/** Options for {@link createPrismaCompatClient}. */
|
|
202
223
|
export interface PrismaCompatOptions {
|
|
@@ -269,12 +290,69 @@ export interface PrismaCompatRawSurface {
|
|
|
269
290
|
$executeRaw(strings: TemplateStringsArray, ...values: unknown[]): Promise<number>;
|
|
270
291
|
$executeRawUnsafe(sql: string, ...params: unknown[]): Promise<number>;
|
|
271
292
|
}
|
|
272
|
-
/**
|
|
293
|
+
/**
|
|
294
|
+
* A Prisma client extension, restricted to the two components this adapter can
|
|
295
|
+
* honour faithfully.
|
|
296
|
+
*
|
|
297
|
+
* `client` members land on the returned client; `model` members land on the
|
|
298
|
+
* named model's delegate (under BOTH spellings), with `$allModels` applying to
|
|
299
|
+
* every delegate. The `query` and `result` components are declared `never` so
|
|
300
|
+
* passing one is a compile error, and {@link PrismaCompatClient.$extends} also
|
|
301
|
+
* refuses them at runtime with an {@link UnsupportedFeatureError} naming the
|
|
302
|
+
* component: an extension that was accepted and then quietly not applied would
|
|
303
|
+
* be far worse than one that is refused.
|
|
304
|
+
*/
|
|
305
|
+
export interface PrismaCompatExtension {
|
|
306
|
+
/** Optional extension name, accepted and otherwise unused (as in Prisma). */
|
|
307
|
+
name?: string;
|
|
308
|
+
/** Extra client-level members, e.g. `{ $healthCheck() { … } }`. */
|
|
309
|
+
client?: Record<string, unknown>;
|
|
310
|
+
/** Extra delegate members per Prisma model name, plus `$allModels`. */
|
|
311
|
+
model?: Record<string, Record<string, unknown>>;
|
|
312
|
+
/** Not supported, see {@link PrismaCompatExtension}. */
|
|
313
|
+
query?: never;
|
|
314
|
+
/** Not supported, see {@link PrismaCompatExtension}. */
|
|
315
|
+
result?: never;
|
|
316
|
+
/** Any other component is refused at runtime by name. */
|
|
317
|
+
[component: string]: unknown;
|
|
318
|
+
}
|
|
319
|
+
/** Members an extension contributes to the delegate for Prisma model `K`. */
|
|
320
|
+
type ModelMembersOf<M, K extends string> = (K extends keyof M ? M[K] : unknown) & (Uncapitalize<K> extends keyof M ? M[Uncapitalize<K>] : unknown) & ('$allModels' extends keyof M ? M['$allModels'] : unknown);
|
|
321
|
+
type ExtraModelMembers<E, K extends string> = E extends {
|
|
322
|
+
model: infer M;
|
|
323
|
+
} ? ModelMembersOf<M, K> : unknown;
|
|
324
|
+
/**
|
|
325
|
+
* The client {@link PrismaCompatClient.$extends} returns: the same surface with
|
|
326
|
+
* the extension's `client` members on the client and its `model` members on
|
|
327
|
+
* every matching delegate (both spellings).
|
|
328
|
+
*/
|
|
329
|
+
export type PrismaCompatExtendedClient<S extends Record<string, PrismaModelTypes>, E> = {
|
|
330
|
+
[K in keyof S]: PrismaModelDelegate<S[K]> & ExtraModelMembers<E, K & string>;
|
|
331
|
+
} & {
|
|
332
|
+
[K in keyof S as Uncapitalize<K & string>]: PrismaModelDelegate<S[K]> & ExtraModelMembers<E, K & string>;
|
|
333
|
+
} & PrismaCompatClientBase<S> & (E extends {
|
|
334
|
+
client: infer C;
|
|
335
|
+
} ? C : unknown);
|
|
336
|
+
/** The client-level surface (`$transaction` / raw / `$extends`), added to the model map. */
|
|
273
337
|
export interface PrismaCompatClientBase<S extends Record<string, PrismaModelTypes> = Record<string, PrismaModelTypes>> extends PrismaCompatRawSurface {
|
|
274
338
|
$transaction<R>(fn: (tx: PrismaCompatTransactionClient<S>) => Promise<R>, options?: PrismaCompatTxOptions): Promise<R>;
|
|
275
339
|
$transaction<P extends readonly PromiseLike<unknown>[]>(promises: readonly [...P]): Promise<{
|
|
276
340
|
[K in keyof P]: Awaited<P[K]>;
|
|
277
341
|
}>;
|
|
342
|
+
/**
|
|
343
|
+
* Prisma's callback form (`Prisma.defineExtension((client) => …)`): the
|
|
344
|
+
* function is called with this client and its return value is the result,
|
|
345
|
+
* exactly as in Prisma. Declared first so a function argument never matches
|
|
346
|
+
* the all-optional object overload below.
|
|
347
|
+
*/
|
|
348
|
+
$extends<R>(extension: (client: PrismaCompatClient<S>) => R): R;
|
|
349
|
+
/**
|
|
350
|
+
* Extend the client with a {@link PrismaCompatExtension}. Returns a NEW client
|
|
351
|
+
* (this one is untouched) carrying the extension's `client` and `model`
|
|
352
|
+
* members; the returned client is itself extendable. `query` and `result`
|
|
353
|
+
* extensions throw, see {@link PrismaCompatExtension}.
|
|
354
|
+
*/
|
|
355
|
+
$extends<E extends PrismaCompatExtension>(extension: E): PrismaCompatExtendedClient<S, E>;
|
|
278
356
|
$connect(): Promise<void>;
|
|
279
357
|
$disconnect(): Promise<void>;
|
|
280
358
|
}
|