@objectstack/types 17.0.0-rc.5 → 17.0.0-rc.6
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/CHANGELOG.md +302 -0
- package/dist/index.d.mts +192 -1
- package/dist/index.d.ts +192 -1
- package/dist/index.js +86 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +81 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,307 @@
|
|
|
1
1
|
# @objectstack/types
|
|
2
2
|
|
|
3
|
+
## 17.0.0-rc.6
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 91cefb8: refactor(types,rest,metadata,analytics): Postgres 的 `"x" of relation "y"` 短语收归一处,三个包不再各修一遍同一个超串洞(#6615)
|
|
8
|
+
|
|
9
|
+
Postgres 把「关系内部某个子对象」的失败写成 `column "label" of relation "sys_team" does not exist`——里面**逐字包含**一句合法的「表不存在」短语 `relation "sys_team" does not exist`,含义却相反:关系正因为存在才被点名。任何对「这句话是不是在说表没了」的正则收紧都消不掉这个匹配,短语确实在里面;唯一的修法是**先问更具体的问题**。所以修的是**顺序**,不是模式。
|
|
10
|
+
|
|
11
|
+
正因为如此,这个短语被分三次教给了这个仓库,分属三个包、三个 PR,其中两次是在别处已经踩过同一个洞之后:`@objectstack/rest` 的 `mapDataError`(#5352)、`@objectstack/service-analytics` 的缺列扣除(#6035 / PR #6346)、`@objectstack/metadata` 的 `MISSING_TABLE.excludes`(#6347 / PR #6613)。本次把它收进 `@objectstack/types`,与 `isUniqueViolationError`(#6250)和 `isModuleNotFoundError`(framework#3265)同一个理由与同一个位置。
|
|
12
|
+
|
|
13
|
+
**两种宽度,故意保留成两个导出。** 三个消费者要的并不是同一条正则,差别也不是随手写的,而是**每个站点哪个方向的误差是安全的**:
|
|
14
|
+
|
|
15
|
+
- `matchMissingColumnOfRelation(message)` —— 严格提取器,锚定 Postgres 的 errmsg 模板 `column "%s" of relation "%s" does not exist`,返回列名。`rest` 用它把 42703 答成 `400 INVALID_FIELD` 而不是 `404`;`service-analytics` 用它在分类前扣除缺列。这两处**过宽**会把真正缺失的表变成硬失败、回退 #5033 刻意保留的宽容,**漏匹配**只是让消息含糊一点——所以必须严格。
|
|
16
|
+
- `isRelationSubObjectPhrase(message)` —— 宽检测器,丢掉 `column` / `[a-z0-9_]+` / `does not exist` 三个锚点:任意子对象、任意带引号标识符、任意判词。`metadata` 用它做排除。这一处**过宽**只会把良性判定变成响亮判定,**漏匹配**却会让 `event_seq` 从 1 重新开始、撞进一张已有行的历史表——方向正好相反。
|
|
17
|
+
|
|
18
|
+
把两者合并成一条正则,无论哪种宽度胜出都会对其中一个调用方是错的;这是卡片记录在案的风险,两个导出即为此而设,理由是承重的而非风格的。仓库里第四份拷贝(`service-analytics` 测试内用于守护 fixture 的那条正则)同时收编:它本是为「两张面孔别对不上」而写,却把断言打在其中一面的私有复述上,因而正是它要防的漂移。
|
|
19
|
+
|
|
20
|
+
行为逐字保持不变:搬进来的两条模式与原站点逐字节相同。`@objectstack/service-analytics` 因此新增一条对 `@objectstack/types` 的依赖边——这是本次唯一的依赖变化,构造上无环(`@objectstack/types` 只依赖 `@objectstack/spec`,后者无仓内依赖),且仓库 73 个包中已有 25 个、16 个 service 中已有 5 个携带同一条边。
|
|
21
|
+
|
|
22
|
+
- 129b378: fix(types,rest): one named answer for "which column conflicted" — an index name is never returned as one (#6544)
|
|
23
|
+
|
|
24
|
+
#6250 retired four private "is this a unique violation?" vocabularies into
|
|
25
|
+
`isUniqueViolationError`. It left the harder half of the question behind: the
|
|
26
|
+
import runner's `sanitizeRowError` still carried its own three-dialect regex
|
|
27
|
+
chain, because it does **more** than answer yes/no — it names the offending
|
|
28
|
+
column so the importer can say _"A record with this `email` already exists."_
|
|
29
|
+
This lands that second answer as a shared export and migrates the last private
|
|
30
|
+
copy onto it.
|
|
31
|
+
|
|
32
|
+
**New — `uniqueViolationColumn(error)` in `@objectstack/types`** (`string |
|
|
33
|
+
undefined`), sibling to `isUniqueViolationError` and gated on it, reading the
|
|
34
|
+
same channels one step down the same bounded `cause` chain, plus
|
|
35
|
+
node-postgres' `detail` field.
|
|
36
|
+
|
|
37
|
+
**Its contract, per the maintainer's 2026-08-08 ruling: a value comes back only
|
|
38
|
+
when the identifier the driver printed is determinably a COLUMN.** When a
|
|
39
|
+
dialect names an _index_ instead — MySQL's `Duplicate entry … for key
|
|
40
|
+
'idx_email_unique'`, Postgres' `violates unique constraint "sys_user_email_key"`,
|
|
41
|
+
SQLite's `UNIQUE constraint failed: index 'x'` — the answer is `undefined`,
|
|
42
|
+
never the index name. Callers render this into a form field, and an index name
|
|
43
|
+
mistaken for a column points the user at a field that does not exist, whereas
|
|
44
|
+
`undefined` degrades to generic copy. A **composite** key (`Key (tenant_id,
|
|
45
|
+
email)=(…)`) is `undefined` for the same reason: there is no single offending
|
|
46
|
+
column, and naming the first is the same class of wrong answer.
|
|
47
|
+
|
|
48
|
+
**⚠️ User-visible change on MySQL imports.** MySQL's duplicate-entry message
|
|
49
|
+
names the index and never the column, so the importer no longer names a column
|
|
50
|
+
there: rows that used to read _"A record with this `idx_email_unique` already
|
|
51
|
+
exists."_ — or, on MySQL 8's table-qualified `for key 'sys_user.email'`, a
|
|
52
|
+
plausible-looking _`email`_ that was still an index name — now read **"A record
|
|
53
|
+
with this value already exists."** That is deliberate and is the accepted cost
|
|
54
|
+
of the ruling. The conflict is still recognised as a conflict; only the naming
|
|
55
|
+
narrowed.
|
|
56
|
+
|
|
57
|
+
Three smaller import messages improve in the same move, all previously wrong
|
|
58
|
+
rather than merely vague:
|
|
59
|
+
|
|
60
|
+
- SQLite's expression/partial-index form used to render as _"A record with this
|
|
61
|
+
**index** already exists."_
|
|
62
|
+
- Postgres' expression index used to render the truncated fragment _"A record
|
|
63
|
+
with this **lower(email** already exists."_
|
|
64
|
+
- A Postgres conflict with no `DETAIL:` line used to fall through to the SQL
|
|
65
|
+
backstop and echo the driver's own sentence — index name included — at the
|
|
66
|
+
importer. It now gets the same generic conflict copy, which is also the exact
|
|
67
|
+
wording `mapDataError` puts in the 409 `UNIQUE_VIOLATION` body, so the
|
|
68
|
+
importer and the API say one thing about one condition.
|
|
69
|
+
|
|
70
|
+
Not changed: the NOT NULL branch, the raw-SQL backstop, and every non-conflict
|
|
71
|
+
message, which pass through exactly as before.
|
|
72
|
+
|
|
73
|
+
### Patch Changes
|
|
74
|
+
|
|
75
|
+
- 88f9d94: fix(types,rest): one named unique-violation predicate — a MySQL conflict is 409 UNIQUE_VIOLATION, not 500 (#6250)
|
|
76
|
+
|
|
77
|
+
**On MySQL, every unique-constraint conflict came back as `500 INTERNAL_ERROR`.**
|
|
78
|
+
The API contract registers `UNIQUE_VIOLATION` as a 409 code
|
|
79
|
+
(`packages/spec/src/api/error-code-ledger.zod.ts`), so a front end had no way to
|
|
80
|
+
tell "this email is already taken" from "the server fell over" — no retry advice,
|
|
81
|
+
no field to point at, and a 5xx in the operator's dashboards for what is an
|
|
82
|
+
ordinary client outcome. SQLite and Postgres deployments never saw it, which is
|
|
83
|
+
why it survived: their conflict prose happens to contain the words the mapping
|
|
84
|
+
looked for.
|
|
85
|
+
|
|
86
|
+
**Cause: the conflict verdict was nested inside a leak heuristic.** REST's 409
|
|
87
|
+
branch lived inside the true-branch of `looksLikeInternalErrorLeak()`, keyed on
|
|
88
|
+
the substrings `unique constraint` / `unique violation`. MySQL says
|
|
89
|
+
`ER_DUP_ENTRY: Duplicate entry '…' for key '…'`, which matches no limb of that
|
|
90
|
+
heuristic, so the conflict never reached the `if` at all and fell out of the
|
|
91
|
+
terminal `UNCLASSIFIED_FAULT`. Two unrelated questions — "is this a conflict?"
|
|
92
|
+
and "would echoing this text leak internals?" — had been fused into one, and
|
|
93
|
+
MySQL is where they disagree.
|
|
94
|
+
|
|
95
|
+
Measured on the previous release, through the real error mapper:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
mysql, bare message 500 INTERNAL_ERROR → 409 UNIQUE_VIOLATION
|
|
99
|
+
mysql, knex-wrapped SQL 500 DATABASE_ERROR → 409 UNIQUE_VIOLATION
|
|
100
|
+
postgres, SQLSTATE only 500 INTERNAL_ERROR → 409 UNIQUE_VIOLATION
|
|
101
|
+
sqlite, message 409 UNIQUE_VIOLATION (unchanged)
|
|
102
|
+
postgres, message 409 UNIQUE_VIOLATION (unchanged)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
So the hole was never MySQL-only: the mapping read one of the two channels
|
|
106
|
+
drivers use. A Postgres error carrying SQLSTATE `23505` with unremarkable prose
|
|
107
|
+
was a 500 as well.
|
|
108
|
+
|
|
109
|
+
**New: `isUniqueViolationError(error)`, exported from `@objectstack/types`.** One
|
|
110
|
+
named predicate replaces the substring test, reading every channel a driver
|
|
111
|
+
uses — `code` (`23505` / `ER_DUP_ENTRY` / `SQLITE_CONSTRAINT_UNIQUE`), `errno`
|
|
112
|
+
(`1062`), the message, and one step down the `cause` chain that pool and
|
|
113
|
+
query-builder layers wrap with. Its vocabulary is the union of the four
|
|
114
|
+
hand-written copies the repo already carried, so routing REST through it cannot
|
|
115
|
+
narrow any verdict clients rely on today; an unrecognised error is never a
|
|
116
|
+
conflict, because a false 409 tells an SDK not to retry and points the user at a
|
|
117
|
+
value that is fine.
|
|
118
|
+
|
|
119
|
+
**The internal-leak classifier is byte-identical.** The fix hoists the conflict
|
|
120
|
+
question out of it rather than widening its criteria, so nothing else it guards
|
|
121
|
+
is reclassified as safe-to-expose. And the 409 body is fixed text: MySQL embeds
|
|
122
|
+
the offending user data in its message (`Duplicate entry 'a@b.com' …`) and
|
|
123
|
+
Postgres the index and column names, none of which reaches the client. The full
|
|
124
|
+
driver text still reaches the server log.
|
|
125
|
+
|
|
126
|
+
No action needed. Clients that already handled `409 UNIQUE_VIOLATION` on SQLite
|
|
127
|
+
and Postgres now receive it on MySQL too.
|
|
128
|
+
|
|
129
|
+
- Updated dependencies [3d5c090]
|
|
130
|
+
- Updated dependencies [e5bd768]
|
|
131
|
+
- Updated dependencies [e027b3e]
|
|
132
|
+
- Updated dependencies [c2429b0]
|
|
133
|
+
- Updated dependencies [445a0c2]
|
|
134
|
+
- Updated dependencies [f6609e6]
|
|
135
|
+
- Updated dependencies [a70358a]
|
|
136
|
+
- Updated dependencies [97e7e3c]
|
|
137
|
+
- Updated dependencies [8828b9e]
|
|
138
|
+
- Updated dependencies [53068c1]
|
|
139
|
+
- Updated dependencies [ee58392]
|
|
140
|
+
- Updated dependencies [f16e54e]
|
|
141
|
+
- Updated dependencies [06be54e]
|
|
142
|
+
- Updated dependencies [259459d]
|
|
143
|
+
- Updated dependencies [3f7f14e]
|
|
144
|
+
- Updated dependencies [6968885]
|
|
145
|
+
- Updated dependencies [eaed61f]
|
|
146
|
+
- Updated dependencies [debe2f6]
|
|
147
|
+
- Updated dependencies [97b0798]
|
|
148
|
+
- Updated dependencies [43a7a8d]
|
|
149
|
+
- Updated dependencies [73f69dc]
|
|
150
|
+
- Updated dependencies [04c56aa]
|
|
151
|
+
- Updated dependencies [b3efeb7]
|
|
152
|
+
- Updated dependencies [ddd075a]
|
|
153
|
+
- Updated dependencies [88154be]
|
|
154
|
+
- Updated dependencies [e8dc61e]
|
|
155
|
+
- Updated dependencies [2f3e793]
|
|
156
|
+
- Updated dependencies [d8e8d9c]
|
|
157
|
+
- Updated dependencies [94e749b]
|
|
158
|
+
- Updated dependencies [ea1d916]
|
|
159
|
+
- Updated dependencies [ae31a19]
|
|
160
|
+
- Updated dependencies [e0f300b]
|
|
161
|
+
- Updated dependencies [62b6a2f]
|
|
162
|
+
- Updated dependencies [5b4780b]
|
|
163
|
+
- Updated dependencies [a933452]
|
|
164
|
+
- Updated dependencies [8140915]
|
|
165
|
+
- Updated dependencies [7b48cf9]
|
|
166
|
+
- Updated dependencies [b5404f4]
|
|
167
|
+
- Updated dependencies [f764691]
|
|
168
|
+
- Updated dependencies [e120a5a]
|
|
169
|
+
- Updated dependencies [e650d67]
|
|
170
|
+
- Updated dependencies [04476e7]
|
|
171
|
+
- Updated dependencies [79228cd]
|
|
172
|
+
- Updated dependencies [b3363e9]
|
|
173
|
+
- Updated dependencies [2ef1807]
|
|
174
|
+
- Updated dependencies [d03fe25]
|
|
175
|
+
- Updated dependencies [2672f85]
|
|
176
|
+
- Updated dependencies [11066f6]
|
|
177
|
+
- Updated dependencies [916af17]
|
|
178
|
+
- Updated dependencies [84c86fb]
|
|
179
|
+
- Updated dependencies [2a2a9fb]
|
|
180
|
+
- Updated dependencies [a2e157c]
|
|
181
|
+
- Updated dependencies [95c4227]
|
|
182
|
+
- Updated dependencies [2a61116]
|
|
183
|
+
- Updated dependencies [d4df105]
|
|
184
|
+
- Updated dependencies [e2798fa]
|
|
185
|
+
- Updated dependencies [0fd8556]
|
|
186
|
+
- Updated dependencies [74155c7]
|
|
187
|
+
- Updated dependencies [6908830]
|
|
188
|
+
- Updated dependencies [8b06bba]
|
|
189
|
+
- Updated dependencies [4c54037]
|
|
190
|
+
- Updated dependencies [0f7157b]
|
|
191
|
+
- Updated dependencies [d9bef45]
|
|
192
|
+
- Updated dependencies [f549a0d]
|
|
193
|
+
- Updated dependencies [82da264]
|
|
194
|
+
- Updated dependencies [9b9b70f]
|
|
195
|
+
- Updated dependencies [f5a9bc2]
|
|
196
|
+
- Updated dependencies [881a3cc]
|
|
197
|
+
- Updated dependencies [ad6317b]
|
|
198
|
+
- Updated dependencies [8a88885]
|
|
199
|
+
- Updated dependencies [5f7669e]
|
|
200
|
+
- Updated dependencies [becbe53]
|
|
201
|
+
- Updated dependencies [b127c8b]
|
|
202
|
+
- Updated dependencies [a80302a]
|
|
203
|
+
- Updated dependencies [474f131]
|
|
204
|
+
- Updated dependencies [050cd82]
|
|
205
|
+
- Updated dependencies [4d552af]
|
|
206
|
+
- Updated dependencies [44d677c]
|
|
207
|
+
- Updated dependencies [c32944d]
|
|
208
|
+
- Updated dependencies [1dd780f]
|
|
209
|
+
- Updated dependencies [c8d6f6e]
|
|
210
|
+
- Updated dependencies [92a67f2]
|
|
211
|
+
- Updated dependencies [9136327]
|
|
212
|
+
- Updated dependencies [bf0ae99]
|
|
213
|
+
- Updated dependencies [cb3b6cd]
|
|
214
|
+
- Updated dependencies [73b7234]
|
|
215
|
+
- Updated dependencies [d2b97c3]
|
|
216
|
+
- Updated dependencies [59b794f]
|
|
217
|
+
- Updated dependencies [fc3a36a]
|
|
218
|
+
- Updated dependencies [69787f0]
|
|
219
|
+
- Updated dependencies [5d022a1]
|
|
220
|
+
- Updated dependencies [042b9ee]
|
|
221
|
+
- Updated dependencies [f549a0d]
|
|
222
|
+
- Updated dependencies [a36db28]
|
|
223
|
+
- Updated dependencies [3f8817a]
|
|
224
|
+
- Updated dependencies [a2443e3]
|
|
225
|
+
- Updated dependencies [e1554b1]
|
|
226
|
+
- Updated dependencies [4856789]
|
|
227
|
+
- Updated dependencies [c3f4916]
|
|
228
|
+
- Updated dependencies [33e0385]
|
|
229
|
+
- Updated dependencies [2205363]
|
|
230
|
+
- Updated dependencies [09fe58d]
|
|
231
|
+
- Updated dependencies [d0a5ceb]
|
|
232
|
+
- Updated dependencies [e18a162]
|
|
233
|
+
- Updated dependencies [d127ff0]
|
|
234
|
+
- Updated dependencies [9b86cf6]
|
|
235
|
+
- Updated dependencies [8825a06]
|
|
236
|
+
- Updated dependencies [5087ac6]
|
|
237
|
+
- Updated dependencies [2d1ddf0]
|
|
238
|
+
- Updated dependencies [354b00f]
|
|
239
|
+
- Updated dependencies [3de535b]
|
|
240
|
+
- Updated dependencies [fe2e15a]
|
|
241
|
+
- Updated dependencies [c6b6bb4]
|
|
242
|
+
- Updated dependencies [2f59da0]
|
|
243
|
+
- Updated dependencies [8ad609c]
|
|
244
|
+
- Updated dependencies [bbee302]
|
|
245
|
+
- Updated dependencies [08863dd]
|
|
246
|
+
- Updated dependencies [56664f5]
|
|
247
|
+
- Updated dependencies [31cbe90]
|
|
248
|
+
- Updated dependencies [90bbf25]
|
|
249
|
+
- Updated dependencies [eb91eba]
|
|
250
|
+
- Updated dependencies [42da73d]
|
|
251
|
+
- Updated dependencies [643b7c7]
|
|
252
|
+
- Updated dependencies [1a15893]
|
|
253
|
+
- Updated dependencies [b70e534]
|
|
254
|
+
- Updated dependencies [2233a85]
|
|
255
|
+
- Updated dependencies [62dd69a]
|
|
256
|
+
- Updated dependencies [e15e679]
|
|
257
|
+
- Updated dependencies [2ab1257]
|
|
258
|
+
- Updated dependencies [4cc4fb7]
|
|
259
|
+
- Updated dependencies [2c26040]
|
|
260
|
+
- Updated dependencies [f758cec]
|
|
261
|
+
- Updated dependencies [78f0be8]
|
|
262
|
+
- Updated dependencies [35f7fb4]
|
|
263
|
+
- Updated dependencies [a5302c7]
|
|
264
|
+
- Updated dependencies [7084313]
|
|
265
|
+
- Updated dependencies [0e043d8]
|
|
266
|
+
- Updated dependencies [dadd1ad]
|
|
267
|
+
- Updated dependencies [2f2e63c]
|
|
268
|
+
- Updated dependencies [486d526]
|
|
269
|
+
- Updated dependencies [89d7b35]
|
|
270
|
+
- Updated dependencies [85ec26d]
|
|
271
|
+
- Updated dependencies [f6476fc]
|
|
272
|
+
- Updated dependencies [4ac12ef]
|
|
273
|
+
- Updated dependencies [b88f5e8]
|
|
274
|
+
- Updated dependencies [42cc219]
|
|
275
|
+
- Updated dependencies [d7e0b42]
|
|
276
|
+
- Updated dependencies [3510e4a]
|
|
277
|
+
- Updated dependencies [aa4b90d]
|
|
278
|
+
- Updated dependencies [54299ca]
|
|
279
|
+
- Updated dependencies [dc61def]
|
|
280
|
+
- Updated dependencies [251e888]
|
|
281
|
+
- Updated dependencies [183b4c4]
|
|
282
|
+
- Updated dependencies [2fdb36e]
|
|
283
|
+
- Updated dependencies [20526f5]
|
|
284
|
+
- Updated dependencies [c5eef1d]
|
|
285
|
+
- Updated dependencies [e0f300b]
|
|
286
|
+
- Updated dependencies [761a0ba]
|
|
287
|
+
- Updated dependencies [be87153]
|
|
288
|
+
- Updated dependencies [60f0dd8]
|
|
289
|
+
- Updated dependencies [a87c5cd]
|
|
290
|
+
- Updated dependencies [a47f338]
|
|
291
|
+
- Updated dependencies [2598216]
|
|
292
|
+
- Updated dependencies [2c7e62d]
|
|
293
|
+
- Updated dependencies [eb7613c]
|
|
294
|
+
- Updated dependencies [ecc9110]
|
|
295
|
+
- Updated dependencies [f7bd4e2]
|
|
296
|
+
- Updated dependencies [361bd5b]
|
|
297
|
+
- Updated dependencies [1818998]
|
|
298
|
+
- Updated dependencies [09ee21c]
|
|
299
|
+
- Updated dependencies [f549a0d]
|
|
300
|
+
- Updated dependencies [3fc2e48]
|
|
301
|
+
- Updated dependencies [e8f435c]
|
|
302
|
+
- Updated dependencies [41610f6]
|
|
303
|
+
- @objectstack/spec@17.0.0-rc.6
|
|
304
|
+
|
|
3
305
|
## 17.0.0-rc.5
|
|
4
306
|
|
|
5
307
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -680,6 +680,197 @@ declare function sendOk(res: EnvelopeResponse, data: unknown, status?: number):
|
|
|
680
680
|
*/
|
|
681
681
|
declare function sendError(res: EnvelopeResponse, status: number, code: ErrorCode, message: string, extra?: Pick<ApiError, 'category' | 'httpStatus' | 'details' | 'requestId'>): void;
|
|
682
682
|
|
|
683
|
+
/**
|
|
684
|
+
* The one home for Postgres' `«sub-object» "x" of relation "y" …` phrasing
|
|
685
|
+
* (#6615).
|
|
686
|
+
*
|
|
687
|
+
* ## The superstring hole, stated once
|
|
688
|
+
*
|
|
689
|
+
* Postgres phrases a failure about something *inside* a relation by naming the
|
|
690
|
+
* relation too:
|
|
691
|
+
*
|
|
692
|
+
* ```
|
|
693
|
+
* column "label" of relation "sys_team" does not exist (42703)
|
|
694
|
+
* constraint "uq_sys_team_name" of relation "sys_team" does not exist (42704)
|
|
695
|
+
* column "environment_id" of relation "sys_metadata" already exists (42701)
|
|
696
|
+
* ```
|
|
697
|
+
*
|
|
698
|
+
* Every one of those **contains a complete, legal missing-TABLE phrase** —
|
|
699
|
+
* `relation "sys_team" does not exist` — as a substring, while meaning the
|
|
700
|
+
* opposite: the relation is right there, which is precisely why it could be
|
|
701
|
+
* named. No amount of tightening a "does this say a relation is missing?"
|
|
702
|
+
* regex can remove that match, because the phrase really is in there. The only
|
|
703
|
+
* repair is to ask the more specific question FIRST. That makes the ORDER the
|
|
704
|
+
* fix, not the pattern — and it is why three packages each grew their own copy
|
|
705
|
+
* of this phrase (#5352, #6035/PR #6346, #6347/PR #6613) before it was given a
|
|
706
|
+
* home.
|
|
707
|
+
*
|
|
708
|
+
* ## Two widths, on purpose — never collapse them
|
|
709
|
+
*
|
|
710
|
+
* The three consumers do not want the same regex, and the difference is not
|
|
711
|
+
* sloppiness: it is **which direction of error is safe** at each site.
|
|
712
|
+
*
|
|
713
|
+
* | consumer | asks | uses | a MISS costs |
|
|
714
|
+
* |:---|:---|:---|:---|
|
|
715
|
+
* | `@objectstack/rest` `mapDataError` (#5352) | which column? | {@link matchMissingColumnOfRelation} | a vaguer message (`404` instead of `400 INVALID_FIELD`) |
|
|
716
|
+
* | `@objectstack/service-analytics` `isMissingSourceError` / `missingSourceRelation` (#6035) | is this a missing COLUMN, so keep it hard? | {@link matchMissingColumnOfRelation} | a mistyped column degrades to a confident empty chart |
|
|
717
|
+
* | `@objectstack/metadata` `MISSING_TABLE.excludes` (#6347) | is this about a sub-object, so not a missing table? | {@link isRelationSubObjectPhrase} | a corruption verdict returns (`event_seq` restarts at 1) |
|
|
718
|
+
*
|
|
719
|
+
* The first two **extract**, so they must be strict: over-matching there would
|
|
720
|
+
* turn a genuinely missing table into a hard failure and regress #5033's
|
|
721
|
+
* deliberate leniency, while under-matching merely keeps today's verdict. The
|
|
722
|
+
* third **excludes**, so it is deliberately wider — any sub-object, any quoted
|
|
723
|
+
* identifier, any verdict — because over-matching there only ever converts a
|
|
724
|
+
* benign verdict into a loud one, and a miss restores data corruption.
|
|
725
|
+
*
|
|
726
|
+
* Collapsing the two into one regex would therefore be wrong for one caller
|
|
727
|
+
* whichever width won. They are two exports for that reason, and the reason is
|
|
728
|
+
* load-bearing rather than stylistic.
|
|
729
|
+
*
|
|
730
|
+
* ## Home
|
|
731
|
+
*
|
|
732
|
+
* `@objectstack/types`, following `isUniqueViolationError`'s move
|
|
733
|
+
* (#6250 — four hand-written answers to one question) and
|
|
734
|
+
* `isModuleNotFoundError`'s (framework#3265 — "single shared owner … so the
|
|
735
|
+
* parallel loaders cannot drift apart"). This module deliberately imports
|
|
736
|
+
* nothing.
|
|
737
|
+
*
|
|
738
|
+
* ⚠️ Unlike #6250, adopting this **does** add one dependency edge:
|
|
739
|
+
* `@objectstack/service-analytics` did not depend on `@objectstack/types`
|
|
740
|
+
* before #6615. It is acyclic by construction — `@objectstack/types` depends
|
|
741
|
+
* only on `@objectstack/spec`, which depends on nothing in-repo, so no package
|
|
742
|
+
* except `spec` itself can form a cycle by consuming it — and 25 of the repo's
|
|
743
|
+
* 73 packages (5 of 16 services) already carry the same edge. Recorded here
|
|
744
|
+
* rather than left for a reader to rediscover.
|
|
745
|
+
*/
|
|
746
|
+
/**
|
|
747
|
+
* Postgres' missing-COLUMN template, strictly. Returns the column name, or
|
|
748
|
+
* `undefined` when the message is not that phrase.
|
|
749
|
+
*
|
|
750
|
+
* Anchored to `column "%s" of relation "%s" does not exist` — the exact errmsg
|
|
751
|
+
* template Postgres emits for SQLSTATE 42703 on the write path
|
|
752
|
+
* (`INSERT` / `UPDATE` / `ALTER`). Both quotes are required because Postgres
|
|
753
|
+
* always emits them here, and requiring them is the safe direction of error for
|
|
754
|
+
* the two consumers that call this.
|
|
755
|
+
*
|
|
756
|
+
* Deliberately narrow in two further ways, both preserved verbatim from the
|
|
757
|
+
* open-coded copies this replaces:
|
|
758
|
+
*
|
|
759
|
+
* - the identifier is `[a-z0-9_]+` (case-insensitive), so a quoted identifier
|
|
760
|
+
* carrying a space or punctuation is NOT matched. Postgres can quote such
|
|
761
|
+
* names; the consumers accept the miss because a miss is the cheap direction.
|
|
762
|
+
* - the relation is `\S+` — quoted or bare, unparsed. This function answers
|
|
763
|
+
* "which COLUMN", never "which relation".
|
|
764
|
+
*
|
|
765
|
+
* The read-path phrasing `column "bogus" does not exist` is a different
|
|
766
|
+
* sentence with no relation in it, so it does not match — and it does not need
|
|
767
|
+
* to: it carries no missing-table substring, which is the whole hole this
|
|
768
|
+
* module exists for.
|
|
769
|
+
*/
|
|
770
|
+
declare function matchMissingColumnOfRelation(message: string): string | undefined;
|
|
771
|
+
/**
|
|
772
|
+
* The same quirk, **wider**: does this message talk about any sub-object of a
|
|
773
|
+
* relation, in any verdict?
|
|
774
|
+
*
|
|
775
|
+
* Drops all three of {@link matchMissingColumnOfRelation}'s anchors — the
|
|
776
|
+
* literal `column`, the `[a-z0-9_]+` identifier shape, and the trailing
|
|
777
|
+
* `does not exist` — so it also recognises `constraint "uq_x" of relation "y"
|
|
778
|
+
* does not exist` (42704), `column "x" of relation "y" already exists` (42701),
|
|
779
|
+
* and every other sub-object Postgres phrases this way.
|
|
780
|
+
*
|
|
781
|
+
* For **exclusion** callers only. A `true` here means "the relation is present,
|
|
782
|
+
* so whatever else this error is, it is not a missing table"; it does not mean
|
|
783
|
+
* the error is benign and it names nothing. Using it to extract would be a
|
|
784
|
+
* category error — there is no capture group precisely so that it cannot be.
|
|
785
|
+
*/
|
|
786
|
+
declare function isRelationSubObjectPhrase(message: string): boolean;
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Whether a thrown driver error is a unique/primary-key constraint violation.
|
|
790
|
+
*
|
|
791
|
+
* Reads all three channels in turn — `code`, `errno`, `message` — then one step
|
|
792
|
+
* down the `cause` chain, because pool and query-builder layers re-throw with
|
|
793
|
+
* the original attached. A plain string is judged on the message channel, so a
|
|
794
|
+
* caller that has already unwrapped `err.message` can pass it straight in.
|
|
795
|
+
*
|
|
796
|
+
* **Unrecognised is always `false`.** The default has to be "not a conflict":
|
|
797
|
+
* a false positive relabels an unrelated failure as the client's fault (a 409
|
|
798
|
+
* an SDK will not retry, pointing at a value that is fine), while a false
|
|
799
|
+
* negative costs only the generic envelope that was the status quo.
|
|
800
|
+
*
|
|
801
|
+
* @param error - the thrown value, of any shape.
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* ```ts
|
|
805
|
+
* catch (error) {
|
|
806
|
+
* if (isUniqueViolationError(error)) return conflict(); // 409 UNIQUE_VIOLATION
|
|
807
|
+
* throw error;
|
|
808
|
+
* }
|
|
809
|
+
* ```
|
|
810
|
+
*/
|
|
811
|
+
declare function isUniqueViolationError(error: unknown): boolean;
|
|
812
|
+
/**
|
|
813
|
+
* Which column a unique-constraint violation was raised on — or `undefined`
|
|
814
|
+
* when the dialect did not determinably name one (#6544).
|
|
815
|
+
*
|
|
816
|
+
* ## The contract, and why it is this narrow
|
|
817
|
+
*
|
|
818
|
+
* **A value comes back only when the identifier the driver printed is
|
|
819
|
+
* determinably a COLUMN.** When a dialect names an *index* instead — MySQL's
|
|
820
|
+
* `Duplicate entry 'a@b.com' for key 'idx_email_unique'`, Postgres'
|
|
821
|
+
* `violates unique constraint "sys_user_email_key"`, SQLite's
|
|
822
|
+
* `UNIQUE constraint failed: index 'idx_lower_email'` — the answer is
|
|
823
|
+
* `undefined`, never the index name.
|
|
824
|
+
*
|
|
825
|
+
* That is the maintainer's 2026-08-08 ruling on #6544, and the reasoning is the
|
|
826
|
+
* caller's, not this module's: **an index name mistaken for a column is worse
|
|
827
|
+
* than no answer at all.**
|
|
828
|
+
*
|
|
829
|
+
* - `@objectstack/rest`'s import runner renders this into a form field —
|
|
830
|
+
* "A record with this `email` already exists." An index name there points
|
|
831
|
+
* the user at a field that does not exist on the object, so they cannot act
|
|
832
|
+
* on it; `undefined` degrades to generic copy, which is merely less helpful.
|
|
833
|
+
* - #5495's autonumber-retry branch asks a yes/no question of the answer —
|
|
834
|
+
* "is the conflicting column the autonumber field?" — and an index name
|
|
835
|
+
* produces a *wrong retry decision*, not a vaguer one.
|
|
836
|
+
*
|
|
837
|
+
* ⛔ **The accepted cost: MySQL deployments usually get no column.** MySQL's
|
|
838
|
+
* duplicate-entry message names the index and never the column, so there is
|
|
839
|
+
* nothing here to read. That is deliberate. Do not "improve" this by deriving a
|
|
840
|
+
* column from an index name (`idx_email_unique` → `email`, or MySQL 8's
|
|
841
|
+
* `for key 'sys_user.email'` → `email`): index names are free-form, a
|
|
842
|
+
* deployment's may match no column at all, and a plausible-looking wrong field
|
|
843
|
+
* is exactly the failure this export exists to avoid. If MySQL must name
|
|
844
|
+
* columns, the answer is a schema lookup of the index — a different, wider
|
|
845
|
+
* contract — not a guess in this function.
|
|
846
|
+
*
|
|
847
|
+
* A **composite** key is `undefined` for the same reason: `Key (tenant_id,
|
|
848
|
+
* email)=(…)` has no single offending column, and naming the first is the same
|
|
849
|
+
* class of wrong answer.
|
|
850
|
+
*
|
|
851
|
+
* ## What it reads
|
|
852
|
+
*
|
|
853
|
+
* Gated on {@link isUniqueViolationError}, so a NOT NULL or FOREIGN KEY failure
|
|
854
|
+
* can never reach the extraction — SQLite's `NOT NULL constraint failed: t.c`
|
|
855
|
+
* shares its shape with the positive and is refused at the gate, not by the
|
|
856
|
+
* patterns. Then `message`, then `detail` (node-postgres keeps its `DETAIL:`
|
|
857
|
+
* line there), then one step down the `cause` chain, bounded exactly as the
|
|
858
|
+
* predicate's walk is. A bare string is read as a message, so a caller holding
|
|
859
|
+
* only `err.message` can pass it straight in.
|
|
860
|
+
*
|
|
861
|
+
* @param error - the thrown value, of any shape.
|
|
862
|
+
* @returns the conflicting column, or `undefined` when none is determinable.
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* ```ts
|
|
866
|
+
* const column = uniqueViolationColumn(error);
|
|
867
|
+
* return column
|
|
868
|
+
* ? `A record with this ${column} already exists.`
|
|
869
|
+
* : 'A record with this value already exists.';
|
|
870
|
+
* ```
|
|
871
|
+
*/
|
|
872
|
+
declare function uniqueViolationColumn(error: unknown): string | undefined;
|
|
873
|
+
|
|
683
874
|
/**
|
|
684
875
|
* [ADR-0120 D5e] The `isolated`-posture install gate for `'global'` uniqueness.
|
|
685
876
|
*
|
|
@@ -904,4 +1095,4 @@ interface RuntimePlugin {
|
|
|
904
1095
|
onStart?: (ctx: RuntimeContext) => void | Promise<void>;
|
|
905
1096
|
}
|
|
906
1097
|
|
|
907
|
-
export { type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, type RuntimeContext, type RuntimePlugin, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declaredIndexUniqueIsGlobal, declaresServerFault, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, globalUniqueFindingId, isMcpServerEnabled, isModuleNotFoundError, isPlatformOwnedObject, keysetWalk, looksLikeInternalErrorLeak, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, sendError, sendOk, stampSearchPinyinEnabled, unconfirmedGlobalUniques };
|
|
1098
|
+
export { type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, type RuntimeContext, type RuntimePlugin, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declaredIndexUniqueIsGlobal, declaresServerFault, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, globalUniqueFindingId, isMcpServerEnabled, isModuleNotFoundError, isPlatformOwnedObject, isRelationSubObjectPhrase, isUniqueViolationError, keysetWalk, looksLikeInternalErrorLeak, matchMissingColumnOfRelation, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, sendError, sendOk, stampSearchPinyinEnabled, unconfirmedGlobalUniques, uniqueViolationColumn };
|