turbine-orm 0.56.0 → 0.58.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.
@@ -0,0 +1,209 @@
1
+ /**
2
+ * turbine-orm, the query-argument OPTION SURFACE as runtime data.
3
+ *
4
+ * ## Why this file exists
5
+ *
6
+ * TypeScript erases interfaces, so `FindManyArgs` does not exist at runtime and
7
+ * any layer that has to decide, key by key, what to do with an args object has
8
+ * to keep its own hand-written list. `turbine-orm/prisma-compat` is exactly
9
+ * such a layer: it builds a FRESH turbine args object out of Prisma-shaped
10
+ * input and copies over the keys it recognizes. Every time core gained a
11
+ * query-level option, that ad-hoc allowlist silently failed to gain it, and the
12
+ * option was accepted by the caller's type-checker and then dropped on the
13
+ * floor. There was no feedback of any kind: no error, no warning, no test.
14
+ *
15
+ * These tables are the fix. Each one is a `Record<keyof SomeArgs<Row>,
16
+ * OptionKind>`, the same mechanism `TURBINE_CONFIG_KEYS` (client.ts) uses for
17
+ * the client-config surface, and it binds the compiler in BOTH directions:
18
+ *
19
+ * - add an option to an arg interface and this file stops compiling until a
20
+ * human classifies it ("Property 'fooMode' is missing in type ..."), so an
21
+ * option can no longer be stranded BY OMISSION;
22
+ * - list a key here that is not on the interface and it fails as an excess
23
+ * property, so a table can never drift into describing an option that does
24
+ * not exist.
25
+ *
26
+ * It deliberately does NOT make "add the option in one place" sufficient: it
27
+ * makes the second edit a BUILD FAILURE rather than a silent drop. That trade is
28
+ * intentional. A passthrough-by-default translator would satisfy the shorter
29
+ * wording and be actively wrong, because two of the options below carry FIELD
30
+ * NAMES in their values (`optimisticLock.field`, `distinctOn.columns`), which a
31
+ * compat layer must rename before core ever sees them. Copying those blind
32
+ * works on a schema whose names happen to coincide and breaks on one that
33
+ * renames a column, i.e. it makes the failure mode depend on the schema.
34
+ *
35
+ * ## THE ONE RULE for classifying a new option
36
+ *
37
+ * Classify a key `'native'` ONLY when its value contains no field, relation,
38
+ * column, or model NAME. If the value names anything in the schema, it is
39
+ * `'prisma'`: a name-translating consumer has to walk it by hand.
40
+ *
41
+ * @module
42
+ */
43
+ export const FIND_UNIQUE_OPTIONS = {
44
+ where: 'prisma',
45
+ select: 'prisma',
46
+ omit: 'prisma',
47
+ // Prisma spells this `include`; forwarding `with` would collide with the
48
+ // translated projection and carry turbine relation names into a Prisma call.
49
+ with: 'nativeAlias',
50
+ // Same key on both surfaces, DIFFERENT value domains ('query' | 'join' vs
51
+ // 'join' | 'batched' | 'auto' | 'flatten'), so the value needs mapping.
52
+ relationLoadStrategy: 'prisma',
53
+ timeout: 'native',
54
+ stableRelationOrder: 'native',
55
+ skipGlobalFilters: 'native',
56
+ includePii: 'native',
57
+ forceCustomPlan: 'native',
58
+ };
59
+ export const FIND_MANY_OPTIONS = {
60
+ where: 'prisma',
61
+ select: 'prisma',
62
+ omit: 'prisma',
63
+ orderBy: 'prisma',
64
+ cursor: 'prisma',
65
+ take: 'prisma',
66
+ distinct: 'prisma',
67
+ relationLoadStrategy: 'prisma',
68
+ with: 'nativeAlias',
69
+ limit: 'nativeAlias',
70
+ offset: 'nativeAlias',
71
+ timeout: 'native',
72
+ stableRelationOrder: 'native',
73
+ skipGlobalFilters: 'native',
74
+ warnOnUnlimited: 'native',
75
+ includePii: 'native',
76
+ forceCustomPlan: 'native',
77
+ };
78
+ export const FIND_MANY_STREAM_OPTIONS = {
79
+ ...FIND_MANY_OPTIONS,
80
+ // No streaming delegate exists on the compat surface, so this is not a known
81
+ // key there and passing it is reported rather than quietly ignored.
82
+ batchSize: 'internal',
83
+ };
84
+ export const CREATE_OPTIONS = {
85
+ data: 'prisma',
86
+ timeout: 'native',
87
+ };
88
+ export const CREATE_MANY_OPTIONS = {
89
+ data: 'prisma',
90
+ skipDuplicates: 'prisma',
91
+ timeout: 'native',
92
+ };
93
+ export const UPDATE_OPTIONS = {
94
+ where: 'prisma',
95
+ data: 'prisma',
96
+ // `{ field, expected }`, and `field` is a FIELD NAME, so it has to be renamed
97
+ // into turbine's naming space rather than copied. See THE ONE RULE above.
98
+ optimisticLock: 'prisma',
99
+ timeout: 'native',
100
+ allowFullTableScan: 'native',
101
+ skipGlobalFilters: 'native',
102
+ };
103
+ export const UPDATE_MANY_OPTIONS = {
104
+ where: 'prisma',
105
+ data: 'prisma',
106
+ timeout: 'native',
107
+ allowFullTableScan: 'native',
108
+ skipGlobalFilters: 'native',
109
+ };
110
+ export const DELETE_OPTIONS = {
111
+ where: 'prisma',
112
+ timeout: 'native',
113
+ allowFullTableScan: 'native',
114
+ skipGlobalFilters: 'native',
115
+ };
116
+ export const DELETE_MANY_OPTIONS = {
117
+ where: 'prisma',
118
+ timeout: 'native',
119
+ allowFullTableScan: 'native',
120
+ skipGlobalFilters: 'native',
121
+ };
122
+ export const UPSERT_OPTIONS = {
123
+ where: 'prisma',
124
+ create: 'prisma',
125
+ update: 'prisma',
126
+ timeout: 'native',
127
+ skipGlobalFilters: 'native',
128
+ };
129
+ export const COUNT_OPTIONS = {
130
+ where: 'prisma',
131
+ timeout: 'native',
132
+ skipGlobalFilters: 'native',
133
+ forceCustomPlan: 'native',
134
+ };
135
+ export const AGGREGATE_OPTIONS = {
136
+ where: 'prisma',
137
+ _count: 'prisma',
138
+ _sum: 'prisma',
139
+ _avg: 'prisma',
140
+ _min: 'prisma',
141
+ _max: 'prisma',
142
+ timeout: 'native',
143
+ skipGlobalFilters: 'native',
144
+ includePii: 'native',
145
+ forceCustomPlan: 'native',
146
+ };
147
+ export const GROUP_BY_OPTIONS = {
148
+ by: 'prisma',
149
+ where: 'prisma',
150
+ having: 'prisma',
151
+ orderBy: 'prisma',
152
+ _count: 'prisma',
153
+ _sum: 'prisma',
154
+ _avg: 'prisma',
155
+ _min: 'prisma',
156
+ _max: 'prisma',
157
+ // `{ columns, orderBy }`, both in FIELD-NAME space. See THE ONE RULE.
158
+ distinctOn: 'prisma',
159
+ limit: 'nativeAlias',
160
+ offset: 'nativeAlias',
161
+ timeout: 'native',
162
+ skipGlobalFilters: 'native',
163
+ includePii: 'native',
164
+ forceCustomPlan: 'native',
165
+ };
166
+ /**
167
+ * Every table, so a test can assert the set is complete and well-formed without
168
+ * naming each one (a table stubbed out during a refactor shows up here).
169
+ */
170
+ export const ALL_OPTION_TABLES = {
171
+ findUnique: FIND_UNIQUE_OPTIONS,
172
+ findMany: FIND_MANY_OPTIONS,
173
+ findManyStream: FIND_MANY_STREAM_OPTIONS,
174
+ create: CREATE_OPTIONS,
175
+ createMany: CREATE_MANY_OPTIONS,
176
+ update: UPDATE_OPTIONS,
177
+ updateMany: UPDATE_MANY_OPTIONS,
178
+ delete: DELETE_OPTIONS,
179
+ deleteMany: DELETE_MANY_OPTIONS,
180
+ upsert: UPSERT_OPTIONS,
181
+ count: COUNT_OPTIONS,
182
+ aggregate: AGGREGATE_OPTIONS,
183
+ groupBy: GROUP_BY_OPTIONS,
184
+ };
185
+ /**
186
+ * Copy every `'native'` key present on `src` onto `dst`.
187
+ *
188
+ * Iterates `src` (a small caller-supplied object) rather than the table, so the
189
+ * cost is proportional to what was actually passed. `undefined` values are
190
+ * skipped: `{ ...maybeOpts }` routinely materializes keys with no value, and
191
+ * writing `undefined` through would be indistinguishable from passing it.
192
+ */
193
+ export function applyNativeOptions(table, src, dst) {
194
+ // Total on any input: a delegate whose args are optional can be called with
195
+ // none, and a diagnostic-adjacent helper must not be the thing that throws.
196
+ if (src === null || typeof src !== 'object')
197
+ return;
198
+ for (const key of Object.keys(src)) {
199
+ if (table[key] !== 'native')
200
+ continue;
201
+ const value = src[key];
202
+ if (value !== undefined)
203
+ dst[key] = value;
204
+ }
205
+ }
206
+ /** The keys of `table` with the given kind, as a set. */
207
+ export function optionKeysOfKind(table, ...kinds) {
208
+ return Object.keys(table).filter((k) => kinds.includes(table[k]));
209
+ }
@@ -420,6 +420,22 @@ export declare function jsonWireCoercionOid(pgType: string | undefined): number
420
420
  export declare function coerceJsonWireValue(oid: number, value: unknown): unknown;
421
421
  /** The closest name in `candidates` to `input`, or null when none is close. */
422
422
  export declare function closestName(input: string, candidates: Iterable<string>): string | null;
423
+ /**
424
+ * The real option key `key` most likely meant, or null when nothing is close.
425
+ *
426
+ * Shared by every "unknown option" diagnostic (the client-config warner in
427
+ * client.ts and the prisma-compat query-option warner), so a reader who has
428
+ * seen one recognizes the ranking in the other.
429
+ *
430
+ * {@link closestName} decides first, which is bounded by edit distance and
431
+ * covers typos. It does not cover the miss these warnings exist for: a guessed
432
+ * name that omits a whole WORD. `logParams` is five edits from `logQueryParams`,
433
+ * past the bound, yet it names the same words in the same order; likewise
434
+ * `customPlan` for `forceCustomPlan`. So a second pass accepts a candidate whose
435
+ * camelCase words CONTAIN the guess's words in order, preferring the one that
436
+ * adds fewest words.
437
+ */
438
+ export declare function suggestKey(key: string, candidates: Iterable<string>): string | null;
423
439
  /**
424
440
  * The "unknown field" error text, listing RELATIONS as well as columns.
425
441
  *
@@ -781,6 +781,55 @@ export function closestName(input, candidates) {
781
781
  }
782
782
  return best;
783
783
  }
784
+ /** camelCase name → its lowercased words (`logQueryParams` → log, query, params). */
785
+ function camelWords(name) {
786
+ return name
787
+ .split(/(?=[A-Z])/)
788
+ .map((w) => w.toLowerCase())
789
+ .filter(Boolean);
790
+ }
791
+ /**
792
+ * The real option key `key` most likely meant, or null when nothing is close.
793
+ *
794
+ * Shared by every "unknown option" diagnostic (the client-config warner in
795
+ * client.ts and the prisma-compat query-option warner), so a reader who has
796
+ * seen one recognizes the ranking in the other.
797
+ *
798
+ * {@link closestName} decides first, which is bounded by edit distance and
799
+ * covers typos. It does not cover the miss these warnings exist for: a guessed
800
+ * name that omits a whole WORD. `logParams` is five edits from `logQueryParams`,
801
+ * past the bound, yet it names the same words in the same order; likewise
802
+ * `customPlan` for `forceCustomPlan`. So a second pass accepts a candidate whose
803
+ * camelCase words CONTAIN the guess's words in order, preferring the one that
804
+ * adds fewest words.
805
+ */
806
+ export function suggestKey(key, candidates) {
807
+ const direct = closestName(key, candidates);
808
+ if (direct)
809
+ return direct;
810
+ const wanted = camelWords(key);
811
+ if (wanted.length < 2)
812
+ return null;
813
+ let best = null;
814
+ let bestExtra = Number.POSITIVE_INFINITY;
815
+ for (const candidate of candidates) {
816
+ const words = camelWords(candidate);
817
+ if (words.length <= wanted.length)
818
+ continue;
819
+ let i = 0;
820
+ for (const w of words)
821
+ if (w === wanted[i])
822
+ i++;
823
+ if (i !== wanted.length)
824
+ continue;
825
+ const extra = words.length - wanted.length;
826
+ if (extra < bestExtra) {
827
+ bestExtra = extra;
828
+ best = candidate;
829
+ }
830
+ }
831
+ return best;
832
+ }
784
833
  /**
785
834
  * The "unknown field" error text, listing RELATIONS as well as columns.
786
835
  *
@@ -96,6 +96,14 @@ export declare const WARN_NS: {
96
96
  * `warnParserOverwrite`). Keyed on the OID.
97
97
  */
98
98
  readonly parserOverwrite: "parserOverwrite";
99
+ /**
100
+ * A key on the args object passed to a `turbine-orm/prisma-compat` delegate
101
+ * call that is neither a Prisma arg for that operation nor a turbine-native
102
+ * query option (prisma-compat.ts `warnUnknownQueryOptions`). Keyed on
103
+ * `model.operation.key`, so the same typo on two models is two reports, and
104
+ * a million executions of one call site is one.
105
+ */
106
+ readonly unknownQueryOption: "unknownQueryOption";
99
107
  /**
100
108
  * `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
101
109
  * runs no connection setup, so the option is a no-op (client.ts constructor).
@@ -131,6 +131,14 @@ export const WARN_NS = {
131
131
  * `warnParserOverwrite`). Keyed on the OID.
132
132
  */
133
133
  parserOverwrite: 'parserOverwrite',
134
+ /**
135
+ * A key on the args object passed to a `turbine-orm/prisma-compat` delegate
136
+ * call that is neither a Prisma arg for that operation nor a turbine-native
137
+ * query option (prisma-compat.ts `warnUnknownQueryOptions`). Keyed on
138
+ * `model.operation.key`, so the same typo on two models is two reports, and
139
+ * a million executions of one call site is one.
140
+ */
141
+ unknownQueryOption: 'unknownQueryOption',
134
142
  /**
135
143
  * `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
136
144
  * runs no connection setup, so the option is a no-op (client.ts constructor).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.56.0",
3
+ "version": "0.58.0",
4
4
  "description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",