@objectstack/plugin-pinyin-search 17.0.0-rc.5 → 17.0.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/CHANGELOG.md +581 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,586 @@
|
|
|
1
1
|
# @objectstack/plugin-pinyin-search
|
|
2
2
|
|
|
3
|
+
## 17.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the
|
|
8
|
+
production-dependency batch from #3517
|
|
9
|
+
|
|
10
|
+
**better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`,
|
|
11
|
+
`@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the
|
|
12
|
+
adapter/telemetry overrides). `@better-auth/scim` deliberately stays on
|
|
13
|
+
1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the
|
|
14
|
+
`scimProvider` model and the generate-token endpoint are gone), which is a
|
|
15
|
+
feature migration, not a version bump. Its peer range accepts rc.2 core, and the
|
|
16
|
+
advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed.
|
|
17
|
+
|
|
18
|
+
**BREAKING — account identity.** better-auth renamed `account.accountId` to
|
|
19
|
+
`account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now
|
|
20
|
+
resolves accounts by `(issuer, providerAccountId)`.
|
|
21
|
+
|
|
22
|
+
- FROM `fields: { accountId: 'account_id' }` → TO
|
|
23
|
+
`fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider
|
|
24
|
+
account id keeps its `account_id` column — only the better-auth-side name
|
|
25
|
+
moved — and `sys_account` gains an `issuer` column.
|
|
26
|
+
- FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO
|
|
27
|
+
`createAccount({ providerId, issuer, providerAccountId, … })`. A local
|
|
28
|
+
password account carries the issuer better-auth mints for itself,
|
|
29
|
+
`local:credential`.
|
|
30
|
+
- FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO
|
|
31
|
+
`unlink({ accountId })`, where `accountId` is now the account ROW id (the `id`
|
|
32
|
+
from `accounts.list()`), matching better-auth's narrowed body.
|
|
33
|
+
`accounts.list()` returns `issuer` + `providerAccountId` in place of
|
|
34
|
+
`accountId`.
|
|
35
|
+
|
|
36
|
+
**Existing deployments:** rows written before 1.7 have no issuer and are
|
|
37
|
+
invisible to sign-in until stamped. The auth plugin now runs an idempotent
|
|
38
|
+
boot-time backfill that stamps what it can derive — `local:credential` for
|
|
39
|
+
password accounts, `local:oauth:<providerId>` for configured social providers,
|
|
40
|
+
and the registered IdP's real `iss` from `sys_sso_provider` for federated ones.
|
|
41
|
+
Accounts from a federated IdP that is no longer registered cannot be derived;
|
|
42
|
+
they are logged with their provider id and row count rather than guessed, and
|
|
43
|
+
those users cannot sign in through that provider until the row is stamped with
|
|
44
|
+
the IdP's issuer or removed so a fresh login re-links it.
|
|
45
|
+
|
|
46
|
+
**Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both
|
|
47
|
+
now implemented over the kernel cache service — `getAndDelete` (single-use
|
|
48
|
+
verification values) and `increment` (fixed-window rate-limit counter;
|
|
49
|
+
`rateLimit.storage: 'secondary-storage'` throws at boot without it).
|
|
50
|
+
|
|
51
|
+
The rest of #3517's production-dependency batch rides along: `@oclif/core`
|
|
52
|
+
4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4,
|
|
53
|
+
`pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps.
|
|
54
|
+
|
|
55
|
+
- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
|
|
56
|
+
|
|
57
|
+
The AGENTS.md post-task checklist requires breaking changesets to carry their
|
|
58
|
+
FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
|
|
59
|
+
inside the npm package and is what an upgrading agent greps after the tombstone
|
|
60
|
+
error." That delivery path was severed for 68 of the 69 publishable packages:
|
|
61
|
+
npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
|
|
62
|
+
older npm versions — not `CHANGELOG.md`, and the canonical
|
|
63
|
+
`"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
|
|
64
|
+
10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
|
|
65
|
+
70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
|
|
66
|
+
explicitly.
|
|
67
|
+
|
|
68
|
+
The tombstone-error scenario is precisely the one where the repo is out of
|
|
69
|
+
reach — the upgrading agent has `node_modules` and nothing else — so the
|
|
70
|
+
migration text has to ride in the tarball. Every publishable package now
|
|
71
|
+
declares `CHANGELOG.md` in `files`, and the canonical whitelist is
|
|
72
|
+
`["dist", "README.md", "CHANGELOG.md"]`.
|
|
73
|
+
|
|
74
|
+
The other half is the gate: `check:published-files` gains a fifth invariant,
|
|
75
|
+
COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
|
|
76
|
+
always-required lint job, so the next package cannot silently sever the path
|
|
77
|
+
again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
|
|
78
|
+
into the canonical set.
|
|
79
|
+
|
|
80
|
+
Consumer-visible change: one more file per install (the package's changelog,
|
|
81
|
+
e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
|
|
82
|
+
node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
|
|
83
|
+
promised.
|
|
84
|
+
|
|
85
|
+
- 9881074: fix(batch): the background walks seek instead of counting, so they stop skipping rows (#4363)
|
|
86
|
+
|
|
87
|
+
#4363 made a single paged read a partition of its result set. It could not make
|
|
88
|
+
a _walk_ one: seven background scans paged with a growing `offset` while writing
|
|
89
|
+
to the very rows they were reading, and an offset counts into a set those writes
|
|
90
|
+
are changing. Rows slide past the cursor and are never visited.
|
|
91
|
+
|
|
92
|
+
That is not a slow page in any of these — it is a wrong answer wearing the shape
|
|
93
|
+
of a clean run:
|
|
94
|
+
|
|
95
|
+
- **`rebuildApproverIndex`** built its desired state by walking
|
|
96
|
+
`sys_approval_request WHERE status = 'pending'` with no `orderBy` at all, then
|
|
97
|
+
**deleted** every index row that state did not explain. A skipped request
|
|
98
|
+
meant an approver silently dropped from someone's queue. (The loop beside it
|
|
99
|
+
ordered by `created_at` — not unique, so its pages were never a partition
|
|
100
|
+
either.)
|
|
101
|
+
- **`verifyFileReferences`** decides which files nothing references. A record it
|
|
102
|
+
never visits is reported as an unreferenced file.
|
|
103
|
+
- **`backfillFileReferences`** and the **pinyin companion backfill** rewrite
|
|
104
|
+
each row they read, so their own writes were shifting the set out from under
|
|
105
|
+
the cursor. Records were left unconverted and unsearchable by a run that
|
|
106
|
+
reported success.
|
|
107
|
+
- **`scanValueShapes`** exists to vouch that no stored value is off-shape, and
|
|
108
|
+
it opens a migration gate on that evidence.
|
|
109
|
+
|
|
110
|
+
All of them now go through `keysetWalk` (`@objectstack/types`): order by a
|
|
111
|
+
unique key, and seek past the last one instead of counting from the start. A
|
|
112
|
+
row's key does not move when the row is updated, and cannot be shifted when
|
|
113
|
+
another is deleted, so the walk is stable under exactly the mutation these
|
|
114
|
+
functions perform. It is also O(n) rather than O(n²/page) — measured on
|
|
115
|
+
Postgres over 2M rows, deep pages cost ~1.1 s by offset against ~0.09 s by seek.
|
|
116
|
+
|
|
117
|
+
One deliberate non-conversion: the REST **export** stream keeps its offset. It
|
|
118
|
+
honors a caller-chosen sort, and a keyset walk would have to re-order the export
|
|
119
|
+
by `id` to seek — changing what the user asked for to fix a cost. Its pages are
|
|
120
|
+
already a partition since #4363; only the depth cost remains.
|
|
121
|
+
|
|
122
|
+
`keysetWalk` merges the cursor with `$and` rather than spreading it into the
|
|
123
|
+
caller's filter, so a walk whose own `where` constrains the key column
|
|
124
|
+
(`{ id: { $in: [...] } }`) keeps that constraint instead of having it silently
|
|
125
|
+
overwritten. When a `max` cap is set it reads one row beyond the cap to tell
|
|
126
|
+
"the cap stopped us" from "the source ended exactly there" — without that, a
|
|
127
|
+
walk that read everything still reports `truncated`, and a caller acting on it
|
|
128
|
+
goes looking for rows that were never withheld.
|
|
129
|
+
|
|
130
|
+
The storage suites' fake engines now **throw** on an `offset` instead of serving
|
|
131
|
+
one, so the conversion is pinned rather than merely passing.
|
|
132
|
+
|
|
133
|
+
- cc2de0e: chore(packaging): 20 packages stop publishing their sources, tests and build tooling (#4248)
|
|
134
|
+
|
|
135
|
+
These 20 packages declared no `files` field, so npm fell back to packing the
|
|
136
|
+
whole package directory. `npm pack --dry-run` on `@objectstack/plugin-webhooks`
|
|
137
|
+
listed **21 files** — 15 under `src/`, three of them unit tests
|
|
138
|
+
(`auto-enqueuer.test.ts`, `bootstrap-declared-webhooks.test.ts`, …), plus the
|
|
139
|
+
build-time `scripts/i18n-extract.config.ts`. `dist/` lands on top of that at
|
|
140
|
+
publish time rather than instead of it, so consumers were installing the
|
|
141
|
+
TypeScript sources and the test suite alongside the artifact they asked for.
|
|
142
|
+
|
|
143
|
+
Each now declares `"files": ["dist", "README.md"]`, matching the 29 packages
|
|
144
|
+
that already did. Nothing a consumer imports moves: every `main` / `types` /
|
|
145
|
+
`exports` target in all 20 already resolved inside `dist/`, which the new
|
|
146
|
+
`check:published-files` guard verifies rather than assumes. The visible change
|
|
147
|
+
is a smaller install and a smaller dependency-scanning surface — `npm pack` on
|
|
148
|
+
`@objectstack/plugin-webhooks` now yields 2 files plus `dist/`.
|
|
149
|
+
|
|
150
|
+
The other half of the fix is the gate. Half the packages declaring `files` and
|
|
151
|
+
half not was the #3786 shape — a hand-copied convention with nothing enforcing
|
|
152
|
+
it, where whoever forgets the line gets no signal at all. `check:published-files`
|
|
153
|
+
(new, wired into the always-required `lint` job) holds every non-private
|
|
154
|
+
workspace package to four invariants: `files` is **declared**; it is
|
|
155
|
+
**sufficient** (covers every entry point, so tightening a whitelist cannot ship
|
|
156
|
+
a package that fails to resolve); it is **minimal** (admits no test, test-harness
|
|
157
|
+
config or build script); and anything beyond `dist` + `README.md` is
|
|
158
|
+
**registered** with a reason, reconciled in both directions so a stale exemption
|
|
159
|
+
is an error rather than dead text. `@objectstack/spec` is the one package with
|
|
160
|
+
registered extras — its `.zod.ts` sources, JSON Schemas, liveness ledgers and
|
|
161
|
+
`CHANGELOG.md` are product, not build input.
|
|
162
|
+
|
|
163
|
+
This also closes an assumption #4206 was resting on. Excluding `<pkg>/scripts/**`
|
|
164
|
+
from the docs-drift implementation test is sound only while no package publishes
|
|
165
|
+
`scripts/` as runtime code; that held, but it held because someone read all three
|
|
166
|
+
offenders by hand. It is now checked on every PR.
|
|
167
|
+
|
|
168
|
+
- 2465133: fix(plugins): sweep the service-lookup erasures out of the plugin composition roots, and fix the two alias-only HTTP reads it exposed (#4251 B5)
|
|
169
|
+
|
|
170
|
+
Batch B5 of the #4251 sweep: the seven remaining `packages/plugins/*` composition
|
|
171
|
+
roots. 35 lookup sites that had been erased to `any` now carry the slot's
|
|
172
|
+
contract, so the compiler checks what each plugin actually calls on the service
|
|
173
|
+
it resolved. The ratchet drops 143 sites / 32 files to 108 / 25.
|
|
174
|
+
|
|
175
|
+
**Two real defects, both of the shape this sweep exists to find.** Approvals'
|
|
176
|
+
actionable-link pages (ADR-0043) and sharing's public share-link REST routes each
|
|
177
|
+
read the HTTP server under `http-server` _only_ — the deprecated alias. The
|
|
178
|
+
ledger records `http.server` as canonical and as the only name present on every
|
|
179
|
+
provider path: `runtime.ts`'s `config.server` path registers no alias at all. On
|
|
180
|
+
that path both lookups threw, the surrounding `catch` swallowed it, and the
|
|
181
|
+
routes silently never mounted — approval e-mail action links 404'd and the
|
|
182
|
+
share-link surface was absent, with nothing in the log to say so. Both reads are
|
|
183
|
+
now canonical-first with the alias as fallback, each name in its own `try`
|
|
184
|
+
because `getService` throws on an empty slot (so `a() ?? b()` inside one `try`
|
|
185
|
+
never reaches `b` — the same correction #4393 made in metadata and
|
|
186
|
+
cloud-connection).
|
|
187
|
+
|
|
188
|
+
Typing choices follow the batch method: pure data-plane consumers take the
|
|
189
|
+
narrow contract (`IDataEngine` in reports), consumers that bind hook or
|
|
190
|
+
middleware seams take the engine seen whole (`IObjectQLEngine` in approvals,
|
|
191
|
+
sharing and pinyin-search), and slots with no contract get a **named** local
|
|
192
|
+
surface rather than `any` — plugin-email's `MailSettingsSurface`, and the
|
|
193
|
+
surfaces the consuming packages already declared (`ApprovalMessagingSurface`,
|
|
194
|
+
`SharingSecurityProbe`, `ReportEmail`). A named surface that omits a member
|
|
195
|
+
still makes the compiler name every call site; `any` says nothing.
|
|
196
|
+
|
|
197
|
+
No behaviour change beyond the two alias reads. No contract changes.
|
|
198
|
+
|
|
199
|
+
- Updated dependencies [690ccf2]
|
|
200
|
+
- Updated dependencies [48fcf70]
|
|
201
|
+
- Updated dependencies [ce5242c]
|
|
202
|
+
- Updated dependencies [257d97a]
|
|
203
|
+
- Updated dependencies [3ec8186]
|
|
204
|
+
- Updated dependencies [6169615]
|
|
205
|
+
- Updated dependencies [fa3d0cf]
|
|
206
|
+
- Updated dependencies [a749273]
|
|
207
|
+
- Updated dependencies [b1863a5]
|
|
208
|
+
- Updated dependencies [fdb4f50]
|
|
209
|
+
- Updated dependencies [270650f]
|
|
210
|
+
- Updated dependencies [956e7f9]
|
|
211
|
+
- Updated dependencies [3aef718]
|
|
212
|
+
- Updated dependencies [879ea13]
|
|
213
|
+
- Updated dependencies [ffb003c]
|
|
214
|
+
- Updated dependencies [32ccb23]
|
|
215
|
+
- Updated dependencies [2d3e255]
|
|
216
|
+
- Updated dependencies [a8940e4]
|
|
217
|
+
- Updated dependencies [7d7521f]
|
|
218
|
+
- Updated dependencies [98877c9]
|
|
219
|
+
- Updated dependencies [f16e54e]
|
|
220
|
+
- Updated dependencies [c44dd5e]
|
|
221
|
+
- Updated dependencies [06be54e]
|
|
222
|
+
- Updated dependencies [28ad90e]
|
|
223
|
+
- Updated dependencies [76d74ec]
|
|
224
|
+
- Updated dependencies [259459d]
|
|
225
|
+
- Updated dependencies [3f7f14e]
|
|
226
|
+
- Updated dependencies [840ee4b]
|
|
227
|
+
- Updated dependencies [64cd010]
|
|
228
|
+
- Updated dependencies [ad4af62]
|
|
229
|
+
- Updated dependencies [debe2f6]
|
|
230
|
+
- Updated dependencies [d44dbfa]
|
|
231
|
+
- Updated dependencies [29c6c9d]
|
|
232
|
+
- Updated dependencies [b949059]
|
|
233
|
+
- Updated dependencies [c5ff96d]
|
|
234
|
+
- Updated dependencies [ad878e7]
|
|
235
|
+
- Updated dependencies [20b1a9e]
|
|
236
|
+
- Updated dependencies [8d895ff]
|
|
237
|
+
- Updated dependencies [3028326]
|
|
238
|
+
- Updated dependencies [ddd075a]
|
|
239
|
+
- Updated dependencies [fe2dfa1]
|
|
240
|
+
- Updated dependencies [c497d26]
|
|
241
|
+
- Updated dependencies [6f6fec7]
|
|
242
|
+
- Updated dependencies [9f5cc79]
|
|
243
|
+
- Updated dependencies [bbdbf28]
|
|
244
|
+
- Updated dependencies [f2445c9]
|
|
245
|
+
- Updated dependencies [ea1d916]
|
|
246
|
+
- Updated dependencies [3905c00]
|
|
247
|
+
- Updated dependencies [4335497]
|
|
248
|
+
- Updated dependencies [2af1988]
|
|
249
|
+
- Updated dependencies [0af50a3]
|
|
250
|
+
- Updated dependencies [1b49eaf]
|
|
251
|
+
- Updated dependencies [db31402]
|
|
252
|
+
- Updated dependencies [2e836de]
|
|
253
|
+
- Updated dependencies [10c4ea9]
|
|
254
|
+
- Updated dependencies [7e5af5c]
|
|
255
|
+
- Updated dependencies [b746aa0]
|
|
256
|
+
- Updated dependencies [d8f65fe]
|
|
257
|
+
- Updated dependencies [58ffcab]
|
|
258
|
+
- Updated dependencies [462b713]
|
|
259
|
+
- Updated dependencies [48c110e]
|
|
260
|
+
- Updated dependencies [87aca93]
|
|
261
|
+
- Updated dependencies [63b33e6]
|
|
262
|
+
- Updated dependencies [a52e2ef]
|
|
263
|
+
- Updated dependencies [eda599e]
|
|
264
|
+
- Updated dependencies [c20b875]
|
|
265
|
+
- Updated dependencies [8e17759]
|
|
266
|
+
- Updated dependencies [2ef1807]
|
|
267
|
+
- Updated dependencies [c519533]
|
|
268
|
+
- Updated dependencies [2a37694]
|
|
269
|
+
- Updated dependencies [b3de0dd]
|
|
270
|
+
- Updated dependencies [32d3800]
|
|
271
|
+
- Updated dependencies [2b63a00]
|
|
272
|
+
- Updated dependencies [55da611]
|
|
273
|
+
- Updated dependencies [3c628ce]
|
|
274
|
+
- Updated dependencies [0fd8556]
|
|
275
|
+
- Updated dependencies [4c45be1]
|
|
276
|
+
- Updated dependencies [6fde910]
|
|
277
|
+
- Updated dependencies [9c82b89]
|
|
278
|
+
- Updated dependencies [60cbf9d]
|
|
279
|
+
- Updated dependencies [ce92674]
|
|
280
|
+
- Updated dependencies [a227ed7]
|
|
281
|
+
- Updated dependencies [74155c7]
|
|
282
|
+
- Updated dependencies [742a6a5]
|
|
283
|
+
- Updated dependencies [1b2eb1b]
|
|
284
|
+
- Updated dependencies [afa6aa5]
|
|
285
|
+
- Updated dependencies [ed77493]
|
|
286
|
+
- Updated dependencies [6908830]
|
|
287
|
+
- Updated dependencies [58a03d2]
|
|
288
|
+
- Updated dependencies [c39d713]
|
|
289
|
+
- Updated dependencies [b7d3be4]
|
|
290
|
+
- Updated dependencies [afb83d3]
|
|
291
|
+
- Updated dependencies [2a0d65e]
|
|
292
|
+
- Updated dependencies [2bacd1a]
|
|
293
|
+
- Updated dependencies [f598aa8]
|
|
294
|
+
- Updated dependencies [82da264]
|
|
295
|
+
- Updated dependencies [f586f1a]
|
|
296
|
+
- Updated dependencies [245d1dc]
|
|
297
|
+
- Updated dependencies [2ad1eba]
|
|
298
|
+
- Updated dependencies [199ec47]
|
|
299
|
+
- Updated dependencies [c7406b0]
|
|
300
|
+
- Updated dependencies [edff010]
|
|
301
|
+
- Updated dependencies [b127c8b]
|
|
302
|
+
- Updated dependencies [763931e]
|
|
303
|
+
- Updated dependencies [ec975f1]
|
|
304
|
+
- Updated dependencies [de9af8a]
|
|
305
|
+
- Updated dependencies [eb4204b]
|
|
306
|
+
- Updated dependencies [91f4c78]
|
|
307
|
+
- Updated dependencies [08f93bc]
|
|
308
|
+
- Updated dependencies [5d4de37]
|
|
309
|
+
- Updated dependencies [0e3a226]
|
|
310
|
+
- Updated dependencies [92a67f2]
|
|
311
|
+
- Updated dependencies [edb4af0]
|
|
312
|
+
- Updated dependencies [f09a2e7]
|
|
313
|
+
- Updated dependencies [eb3e650]
|
|
314
|
+
- Updated dependencies [45dc446]
|
|
315
|
+
- Updated dependencies [cb5a75e]
|
|
316
|
+
- Updated dependencies [84b6e58]
|
|
317
|
+
- Updated dependencies [f160ba4]
|
|
318
|
+
- Updated dependencies [fc3a36a]
|
|
319
|
+
- Updated dependencies [ab9fb5c]
|
|
320
|
+
- Updated dependencies [69787f0]
|
|
321
|
+
- Updated dependencies [5d022a1]
|
|
322
|
+
- Updated dependencies [290d944]
|
|
323
|
+
- Updated dependencies [55011af]
|
|
324
|
+
- Updated dependencies [b25a116]
|
|
325
|
+
- Updated dependencies [02dc076]
|
|
326
|
+
- Updated dependencies [f985b3f]
|
|
327
|
+
- Updated dependencies [127f091]
|
|
328
|
+
- Updated dependencies [518ca7a]
|
|
329
|
+
- Updated dependencies [4cca74c]
|
|
330
|
+
- Updated dependencies [3f8817a]
|
|
331
|
+
- Updated dependencies [53ef057]
|
|
332
|
+
- Updated dependencies [81ce41a]
|
|
333
|
+
- Updated dependencies [85e1e4e]
|
|
334
|
+
- Updated dependencies [3670cf9]
|
|
335
|
+
- Updated dependencies [7f1a635]
|
|
336
|
+
- Updated dependencies [4c5e80e]
|
|
337
|
+
- Updated dependencies [fce8e49]
|
|
338
|
+
- Updated dependencies [5d3ced9]
|
|
339
|
+
- Updated dependencies [0f2fdcd]
|
|
340
|
+
- Updated dependencies [d6d1a50]
|
|
341
|
+
- Updated dependencies [8ffa8b9]
|
|
342
|
+
- Updated dependencies [674ac99]
|
|
343
|
+
- Updated dependencies [833b512]
|
|
344
|
+
- Updated dependencies [9881074]
|
|
345
|
+
- Updated dependencies [1eadac0]
|
|
346
|
+
- Updated dependencies [7777e8f]
|
|
347
|
+
- Updated dependencies [c804f19]
|
|
348
|
+
- Updated dependencies [7c2f7dd]
|
|
349
|
+
- Updated dependencies [9b26699]
|
|
350
|
+
- Updated dependencies [507b92a]
|
|
351
|
+
- Updated dependencies [46365ab]
|
|
352
|
+
- Updated dependencies [dbe92a7]
|
|
353
|
+
- Updated dependencies [df95346]
|
|
354
|
+
- Updated dependencies [e1fa8d5]
|
|
355
|
+
- Updated dependencies [402f534]
|
|
356
|
+
- Updated dependencies [114e727]
|
|
357
|
+
- Updated dependencies [7372d46]
|
|
358
|
+
- Updated dependencies [5e247fd]
|
|
359
|
+
- Updated dependencies [a6cd2c1]
|
|
360
|
+
- Updated dependencies [fc3a819]
|
|
361
|
+
- Updated dependencies [1a53a02]
|
|
362
|
+
- Updated dependencies [75fd301]
|
|
363
|
+
- Updated dependencies [fdca3a1]
|
|
364
|
+
- Updated dependencies [1507ba3]
|
|
365
|
+
- Updated dependencies [9051802]
|
|
366
|
+
- Updated dependencies [1c625ca]
|
|
367
|
+
- Updated dependencies [9319586]
|
|
368
|
+
- Updated dependencies [39eb01b]
|
|
369
|
+
- Updated dependencies [071d0dc]
|
|
370
|
+
- Updated dependencies [bf42e76]
|
|
371
|
+
- Updated dependencies [edbf873]
|
|
372
|
+
- Updated dependencies [c5adfe1]
|
|
373
|
+
- Updated dependencies [1cae606]
|
|
374
|
+
- Updated dependencies [9514767]
|
|
375
|
+
- Updated dependencies [e3c8ed0]
|
|
376
|
+
- Updated dependencies [fa6dd59]
|
|
377
|
+
- Updated dependencies [55bbefc]
|
|
378
|
+
- Updated dependencies [030125b]
|
|
379
|
+
- Updated dependencies [7ce02eb]
|
|
380
|
+
- Updated dependencies [f1da948]
|
|
381
|
+
- Updated dependencies [b9cc17d]
|
|
382
|
+
- Updated dependencies [255f2d7]
|
|
383
|
+
- Updated dependencies [bfe689b]
|
|
384
|
+
- Updated dependencies [d0d5205]
|
|
385
|
+
- Updated dependencies [0d24078]
|
|
386
|
+
- Updated dependencies [8f1851e]
|
|
387
|
+
- Updated dependencies [b4b2c7d]
|
|
388
|
+
- Updated dependencies [de43f94]
|
|
389
|
+
- Updated dependencies [5b8f95b]
|
|
390
|
+
- Updated dependencies [d13004a]
|
|
391
|
+
- Updated dependencies [be7360c]
|
|
392
|
+
- Updated dependencies [2ddba89]
|
|
393
|
+
- Updated dependencies [4b0ebdb]
|
|
394
|
+
- Updated dependencies [28d1eb7]
|
|
395
|
+
- Updated dependencies [06770c0]
|
|
396
|
+
- Updated dependencies [8675db6]
|
|
397
|
+
- Updated dependencies [b09d8d9]
|
|
398
|
+
- Updated dependencies [37a8f2b]
|
|
399
|
+
- Updated dependencies [e50e479]
|
|
400
|
+
- Updated dependencies [c41828d]
|
|
401
|
+
- Updated dependencies [3fb42d2]
|
|
402
|
+
- Updated dependencies [8e08bc3]
|
|
403
|
+
- Updated dependencies [441d79f]
|
|
404
|
+
- Updated dependencies [82397b6]
|
|
405
|
+
- Updated dependencies [4df747c]
|
|
406
|
+
- Updated dependencies [7084313]
|
|
407
|
+
- Updated dependencies [31fb03d]
|
|
408
|
+
- Updated dependencies [47a4e67]
|
|
409
|
+
- Updated dependencies [91cefb8]
|
|
410
|
+
- Updated dependencies [9bc846b]
|
|
411
|
+
- Updated dependencies [af2a095]
|
|
412
|
+
- Updated dependencies [bf478e1]
|
|
413
|
+
- Updated dependencies [77fadbf]
|
|
414
|
+
- Updated dependencies [8dd98bf]
|
|
415
|
+
- Updated dependencies [4fedb11]
|
|
416
|
+
- Updated dependencies [5c13368]
|
|
417
|
+
- Updated dependencies [1ee48bc]
|
|
418
|
+
- Updated dependencies [d13f627]
|
|
419
|
+
- Updated dependencies [a841151]
|
|
420
|
+
- Updated dependencies [4e74c18]
|
|
421
|
+
- Updated dependencies [8b90d68]
|
|
422
|
+
- Updated dependencies [478f1fd]
|
|
423
|
+
- Updated dependencies [833ed84]
|
|
424
|
+
- Updated dependencies [2680cd3]
|
|
425
|
+
- Updated dependencies [0c302a7]
|
|
426
|
+
- Updated dependencies [1788e19]
|
|
427
|
+
- Updated dependencies [857a6cf]
|
|
428
|
+
- Updated dependencies [e474853]
|
|
429
|
+
- Updated dependencies [d5749d7]
|
|
430
|
+
- Updated dependencies [1f6ed16]
|
|
431
|
+
- Updated dependencies [db2ea82]
|
|
432
|
+
- Updated dependencies [51a587d]
|
|
433
|
+
- Updated dependencies [d92c72d]
|
|
434
|
+
- Updated dependencies [5f0852f]
|
|
435
|
+
- Updated dependencies [cde1975]
|
|
436
|
+
- Updated dependencies [20cb232]
|
|
437
|
+
- Updated dependencies [e231abb]
|
|
438
|
+
- Updated dependencies [5d21a48]
|
|
439
|
+
- Updated dependencies [23dba62]
|
|
440
|
+
- Updated dependencies [ee264b2]
|
|
441
|
+
- Updated dependencies [d56bcdb]
|
|
442
|
+
- Updated dependencies [26bb053]
|
|
443
|
+
- Updated dependencies [ee3bde1]
|
|
444
|
+
- Updated dependencies [098b629]
|
|
445
|
+
- Updated dependencies [d86815e]
|
|
446
|
+
- Updated dependencies [0a5dc29]
|
|
447
|
+
- Updated dependencies [e13fd91]
|
|
448
|
+
- Updated dependencies [01a7337]
|
|
449
|
+
- Updated dependencies [50185a8]
|
|
450
|
+
- Updated dependencies [d6bd5a1]
|
|
451
|
+
- Updated dependencies [2bd4e5e]
|
|
452
|
+
- Updated dependencies [1363084]
|
|
453
|
+
- Updated dependencies [488b66c]
|
|
454
|
+
- Updated dependencies [148d451]
|
|
455
|
+
- Updated dependencies [e4c2dc8]
|
|
456
|
+
- Updated dependencies [694c350]
|
|
457
|
+
- Updated dependencies [6ad13bb]
|
|
458
|
+
- Updated dependencies [129b378]
|
|
459
|
+
- Updated dependencies [88f9d94]
|
|
460
|
+
- Updated dependencies [551f899]
|
|
461
|
+
- Updated dependencies [a0fdc56]
|
|
462
|
+
- Updated dependencies [946a131]
|
|
463
|
+
- Updated dependencies [909895d]
|
|
464
|
+
- Updated dependencies [b95577a]
|
|
465
|
+
- Updated dependencies [54f479a]
|
|
466
|
+
- Updated dependencies [ad5fe25]
|
|
467
|
+
- Updated dependencies [1818998]
|
|
468
|
+
- Updated dependencies [89be40c]
|
|
469
|
+
- Updated dependencies [f46e987]
|
|
470
|
+
- Updated dependencies [e3a6f6e]
|
|
471
|
+
- Updated dependencies [c9bf940]
|
|
472
|
+
- Updated dependencies [a682670]
|
|
473
|
+
- Updated dependencies [2b52bc8]
|
|
474
|
+
- Updated dependencies [3556b67]
|
|
475
|
+
- @objectstack/objectql@17.0.0
|
|
476
|
+
- @objectstack/core@17.0.0
|
|
477
|
+
- @objectstack/types@17.0.0
|
|
478
|
+
|
|
479
|
+
## 17.0.0-rc.6
|
|
480
|
+
|
|
481
|
+
### Patch Changes
|
|
482
|
+
|
|
483
|
+
- 2465133: fix(plugins): sweep the service-lookup erasures out of the plugin composition roots, and fix the two alias-only HTTP reads it exposed (#4251 B5)
|
|
484
|
+
|
|
485
|
+
Batch B5 of the #4251 sweep: the seven remaining `packages/plugins/*` composition
|
|
486
|
+
roots. 35 lookup sites that had been erased to `any` now carry the slot's
|
|
487
|
+
contract, so the compiler checks what each plugin actually calls on the service
|
|
488
|
+
it resolved. The ratchet drops 143 sites / 32 files to 108 / 25.
|
|
489
|
+
|
|
490
|
+
**Two real defects, both of the shape this sweep exists to find.** Approvals'
|
|
491
|
+
actionable-link pages (ADR-0043) and sharing's public share-link REST routes each
|
|
492
|
+
read the HTTP server under `http-server` _only_ — the deprecated alias. The
|
|
493
|
+
ledger records `http.server` as canonical and as the only name present on every
|
|
494
|
+
provider path: `runtime.ts`'s `config.server` path registers no alias at all. On
|
|
495
|
+
that path both lookups threw, the surrounding `catch` swallowed it, and the
|
|
496
|
+
routes silently never mounted — approval e-mail action links 404'd and the
|
|
497
|
+
share-link surface was absent, with nothing in the log to say so. Both reads are
|
|
498
|
+
now canonical-first with the alias as fallback, each name in its own `try`
|
|
499
|
+
because `getService` throws on an empty slot (so `a() ?? b()` inside one `try`
|
|
500
|
+
never reaches `b` — the same correction #4393 made in metadata and
|
|
501
|
+
cloud-connection).
|
|
502
|
+
|
|
503
|
+
Typing choices follow the batch method: pure data-plane consumers take the
|
|
504
|
+
narrow contract (`IDataEngine` in reports), consumers that bind hook or
|
|
505
|
+
middleware seams take the engine seen whole (`IObjectQLEngine` in approvals,
|
|
506
|
+
sharing and pinyin-search), and slots with no contract get a **named** local
|
|
507
|
+
surface rather than `any` — plugin-email's `MailSettingsSurface`, and the
|
|
508
|
+
surfaces the consuming packages already declared (`ApprovalMessagingSurface`,
|
|
509
|
+
`SharingSecurityProbe`, `ReportEmail`). A named surface that omits a member
|
|
510
|
+
still makes the compiler name every call site; `any` says nothing.
|
|
511
|
+
|
|
512
|
+
No behaviour change beyond the two alias reads. No contract changes.
|
|
513
|
+
|
|
514
|
+
- Updated dependencies [f16e54e]
|
|
515
|
+
- Updated dependencies [06be54e]
|
|
516
|
+
- Updated dependencies [259459d]
|
|
517
|
+
- Updated dependencies [3f7f14e]
|
|
518
|
+
- Updated dependencies [debe2f6]
|
|
519
|
+
- Updated dependencies [ad878e7]
|
|
520
|
+
- Updated dependencies [3028326]
|
|
521
|
+
- Updated dependencies [ddd075a]
|
|
522
|
+
- Updated dependencies [fe2dfa1]
|
|
523
|
+
- Updated dependencies [6f6fec7]
|
|
524
|
+
- Updated dependencies [ea1d916]
|
|
525
|
+
- Updated dependencies [10c4ea9]
|
|
526
|
+
- Updated dependencies [2ef1807]
|
|
527
|
+
- Updated dependencies [55da611]
|
|
528
|
+
- Updated dependencies [0fd8556]
|
|
529
|
+
- Updated dependencies [6fde910]
|
|
530
|
+
- Updated dependencies [9c82b89]
|
|
531
|
+
- Updated dependencies [74155c7]
|
|
532
|
+
- Updated dependencies [742a6a5]
|
|
533
|
+
- Updated dependencies [6908830]
|
|
534
|
+
- Updated dependencies [b7d3be4]
|
|
535
|
+
- Updated dependencies [2a0d65e]
|
|
536
|
+
- Updated dependencies [82da264]
|
|
537
|
+
- Updated dependencies [f586f1a]
|
|
538
|
+
- Updated dependencies [b127c8b]
|
|
539
|
+
- Updated dependencies [92a67f2]
|
|
540
|
+
- Updated dependencies [edb4af0]
|
|
541
|
+
- Updated dependencies [f09a2e7]
|
|
542
|
+
- Updated dependencies [fc3a36a]
|
|
543
|
+
- Updated dependencies [69787f0]
|
|
544
|
+
- Updated dependencies [5d022a1]
|
|
545
|
+
- Updated dependencies [55011af]
|
|
546
|
+
- Updated dependencies [3f8817a]
|
|
547
|
+
- Updated dependencies [53ef057]
|
|
548
|
+
- Updated dependencies [d6d1a50]
|
|
549
|
+
- Updated dependencies [c804f19]
|
|
550
|
+
- Updated dependencies [dbe92a7]
|
|
551
|
+
- Updated dependencies [114e727]
|
|
552
|
+
- Updated dependencies [5e247fd]
|
|
553
|
+
- Updated dependencies [1a53a02]
|
|
554
|
+
- Updated dependencies [1507ba3]
|
|
555
|
+
- Updated dependencies [bf42e76]
|
|
556
|
+
- Updated dependencies [bfe689b]
|
|
557
|
+
- Updated dependencies [d0d5205]
|
|
558
|
+
- Updated dependencies [de43f94]
|
|
559
|
+
- Updated dependencies [28d1eb7]
|
|
560
|
+
- Updated dependencies [3fb42d2]
|
|
561
|
+
- Updated dependencies [82397b6]
|
|
562
|
+
- Updated dependencies [4df747c]
|
|
563
|
+
- Updated dependencies [7084313]
|
|
564
|
+
- Updated dependencies [47a4e67]
|
|
565
|
+
- Updated dependencies [91cefb8]
|
|
566
|
+
- Updated dependencies [9bc846b]
|
|
567
|
+
- Updated dependencies [4fedb11]
|
|
568
|
+
- Updated dependencies [d13f627]
|
|
569
|
+
- Updated dependencies [a841151]
|
|
570
|
+
- Updated dependencies [1788e19]
|
|
571
|
+
- Updated dependencies [1f6ed16]
|
|
572
|
+
- Updated dependencies [d86815e]
|
|
573
|
+
- Updated dependencies [e13fd91]
|
|
574
|
+
- Updated dependencies [2bd4e5e]
|
|
575
|
+
- Updated dependencies [129b378]
|
|
576
|
+
- Updated dependencies [88f9d94]
|
|
577
|
+
- Updated dependencies [1818998]
|
|
578
|
+
- Updated dependencies [c9bf940]
|
|
579
|
+
- Updated dependencies [a682670]
|
|
580
|
+
- @objectstack/objectql@17.0.0-rc.6
|
|
581
|
+
- @objectstack/core@17.0.0-rc.6
|
|
582
|
+
- @objectstack/types@17.0.0-rc.6
|
|
583
|
+
|
|
3
584
|
## 17.0.0-rc.5
|
|
4
585
|
|
|
5
586
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -37,6 +37,12 @@ declare class PinyinSearchPlugin implements Plugin {
|
|
|
37
37
|
private get enabled();
|
|
38
38
|
init(_ctx: PluginContext): Promise<void>;
|
|
39
39
|
start(ctx: PluginContext): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* [#4251 B5] The engine SEEN WHOLE — `bindSearchCompanionHooks` binds
|
|
42
|
+
* `registerHook`, which lives on `IObjectQLEngine`, not on the data plane.
|
|
43
|
+
* The ledger records `objectql` as "the SAME instance as `data`, seen whole",
|
|
44
|
+
* so the alias fallback resolves the same object under the same contract.
|
|
45
|
+
*/
|
|
40
46
|
private resolveEngine;
|
|
41
47
|
}
|
|
42
48
|
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,12 @@ declare class PinyinSearchPlugin implements Plugin {
|
|
|
37
37
|
private get enabled();
|
|
38
38
|
init(_ctx: PluginContext): Promise<void>;
|
|
39
39
|
start(ctx: PluginContext): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* [#4251 B5] The engine SEEN WHOLE — `bindSearchCompanionHooks` binds
|
|
42
|
+
* `registerHook`, which lives on `IObjectQLEngine`, not on the data plane.
|
|
43
|
+
* The ledger records `objectql` as "the SAME instance as `data`, seen whole",
|
|
44
|
+
* so the alias fallback resolves the same object under the same contract.
|
|
45
|
+
*/
|
|
40
46
|
private resolveEngine;
|
|
41
47
|
}
|
|
42
48
|
|
package/dist/index.js
CHANGED
|
@@ -207,6 +207,12 @@ var PinyinSearchPlugin = class {
|
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* [#4251 B5] The engine SEEN WHOLE — `bindSearchCompanionHooks` binds
|
|
212
|
+
* `registerHook`, which lives on `IObjectQLEngine`, not on the data plane.
|
|
213
|
+
* The ledger records `objectql` as "the SAME instance as `data`, seen whole",
|
|
214
|
+
* so the alias fallback resolves the same object under the same contract.
|
|
215
|
+
*/
|
|
210
216
|
resolveEngine(ctx) {
|
|
211
217
|
try {
|
|
212
218
|
return ctx.getService("objectql");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/pinyin-search-plugin.ts","../src/companion-projection.ts","../src/pinyin.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { PinyinSearchPlugin } from './pinyin-search-plugin.js';\nexport type { PinyinSearchPluginOptions } from './pinyin-search-plugin.js';\nexport {\n bindSearchCompanionHooks,\n backfillSearchCompanion,\n rebuildSearchCompanion,\n PINYIN_SEARCH_HOOK_PACKAGE,\n} from './companion-projection.js';\nexport type {\n CompanionBackfillOptions,\n CompanionBackfillResult,\n} from './companion-projection.js';\nexport { computeSearchCompanionValue } from './pinyin.js';\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * PinyinSearchPlugin (#2486) — pinyin recall for `$search`.\n *\n * Pure hook plugin: the `__search` companion column is declared at object\n * compile time by the SchemaRegistry (gated on the SAME\n * `OS_SEARCH_PINYIN_ENABLED` decision point), the engine ORs it into the\n * `$search` filter, and this plugin fills the value:\n *\n * - before-save hooks: recompute full pinyin + initials of the\n * display/name field when it changes;\n * - boot backfill (`kernel:bootstrapped`): fill rows that predate the\n * switch or arrived via hook-bypassing writes;\n * - `rebuildSearchCompanion` (exported): explicit reconcile/rebuild entry.\n *\n * When the flag is off the plugin is inert: no hooks, no backfill, and\n * `pinyin-pro` is never imported.\n */\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { resolveSearchPinyinEnabled } from '@objectstack/types';\nimport {\n bindSearchCompanionHooks,\n backfillSearchCompanion,\n} from './companion-projection.js';\n\nexport interface PinyinSearchPluginOptions {\n /**\n * Force-enable/disable regardless of `OS_SEARCH_PINYIN_ENABLED` (tests /\n * embedders). Default: `resolveSearchPinyinEnabled()`.\n */\n enabled?: boolean;\n /** Skip the boot backfill (default: run it once per boot). */\n backfill?: boolean;\n}\n\nexport class PinyinSearchPlugin implements Plugin {\n name = 'com.objectstack.plugin.pinyin-search';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: PinyinSearchPluginOptions;\n\n constructor(options: PinyinSearchPluginOptions = {}) {\n this.options = options;\n }\n\n private get enabled(): boolean {\n return this.options.enabled ?? resolveSearchPinyinEnabled();\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // Nothing to register: the companion column is provisioned by the\n // SchemaRegistry's compile-time seam, not injected at runtime.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (!this.enabled) {\n ctx.logger.debug?.('PinyinSearchPlugin: OS_SEARCH_PINYIN_ENABLED is off — inert');\n return;\n }\n\n ctx.hook('kernel:ready', async () => {\n const engine = this.resolveEngine(ctx);\n if (!engine) {\n ctx.logger.warn('PinyinSearchPlugin: no ObjectQL engine — companion hooks NOT bound');\n return;\n }\n try {\n bindSearchCompanionHooks(engine, ctx.logger as any);\n } catch (err: any) {\n ctx.logger.warn('PinyinSearchPlugin: companion hooks not bound', { error: err?.message });\n }\n });\n\n // Backfill AFTER boot settles (`kernel:bootstrapped` fires once every\n // `kernel:ready` hook — including seed loading — has completed), so\n // seeded rows written before/around hook binding are reconciled too.\n if (this.options.backfill !== false) {\n ctx.hook('kernel:bootstrapped', async () => {\n const engine = this.resolveEngine(ctx);\n if (!engine) return;\n try {\n await backfillSearchCompanion(engine, ctx.logger as any);\n } catch (err: any) {\n ctx.logger.warn('PinyinSearchPlugin: companion backfill failed', { error: err?.message });\n }\n });\n }\n }\n\n private resolveEngine(ctx: PluginContext): any {\n try {\n return ctx.getService<any>('objectql');\n } catch {\n try {\n return ctx.getService<any>('data');\n } catch {\n return null;\n }\n }\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `__search` companion-column projection (#2486).\n *\n * The column itself is DECLARED at object compile time by the SchemaRegistry\n * (`provisionSearchCompanion`, gated on `OS_SEARCH_PINYIN_ENABLED`); this\n * module only FILLS the value — the `plugin-sharing` primary-BU projection\n * pattern (column on the object, plugin maintains it via hooks).\n *\n * Write path: global `beforeInsert`/`beforeUpdate` hooks stamp\n * `data.__search` whenever a companion source field (the object's\n * display/name field) is present in the write — i.e. only when the source\n * actually changed, avoiding write amplification. Writes that bypass hooks\n * (bulk import, direct migration) leave the companion empty; the boot\n * backfill and the `rebuildSearchCompanion` reconcile entry cover that.\n */\n\nimport {\n SEARCH_COMPANION_FIELD,\n resolveSearchCompanionSources,\n containsCJK,\n} from '@objectstack/objectql';\nimport { keysetWalk } from '@objectstack/types';\nimport { computeSearchCompanionValue } from './pinyin.js';\n\nconst SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nexport const PINYIN_SEARCH_HOOK_PACKAGE = 'plugin-pinyin-search:companion';\n\ninterface MinimalEngine {\n registerHook(\n event: string,\n handler: (ctx: any) => any | Promise<any>,\n options?: { object?: string | string[]; priority?: number; packageId?: string },\n ): void;\n unregisterHooksByPackage(packageId: string): number;\n find(object: string, query?: any, options?: any): Promise<any[]>;\n update(object: string, data: any, options?: any): Promise<any>;\n registry?: {\n getObject(name: string): any;\n getAllObjects?(packageId?: string): any[];\n };\n}\n\ninterface MinimalLogger {\n info?: (msg: any, ...rest: any[]) => void;\n warn?: (msg: any, ...rest: any[]) => void;\n debug?: (msg: any, ...rest: any[]) => void;\n}\n\n/**\n * Stamp `data.__search` on a before-save hook context when a companion source\n * field is part of the write. Recomputes from the NEW value; a non-CJK new\n * value clears the companion (null) so stale pinyin never recalls a renamed\n * record. Never throws — a normalization failure must not fail the write.\n */\nasync function stampCompanion(engine: MinimalEngine, ctx: any, logger?: MinimalLogger): Promise<void> {\n const object = ctx?.object;\n if (!object) return;\n const schema = engine.registry?.getObject?.(object);\n if (!schema?.fields?.[SEARCH_COMPANION_FIELD]) return;\n\n const data = ctx?.input?.data;\n if (!data || typeof data !== 'object' || Array.isArray(data)) return;\n\n const sources = resolveSearchCompanionSources(schema);\n if (sources.length === 0) return;\n const touched = sources.filter((s) => Object.prototype.hasOwnProperty.call(data, s));\n if (touched.length === 0) return; // source unchanged → no recompute (no write amplification)\n\n try {\n data[SEARCH_COMPANION_FIELD] = await computeSearchCompanionValue(sources.map((s) => data[s]));\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] companion normalization failed — write proceeds without it', {\n object,\n error: err?.message,\n });\n }\n}\n\n/**\n * Bind the global before-save hooks that keep `__search` in step. Idempotent\n * (unbinds the package first). Hooks are global (no object filter) with a\n * cheap early-out: objects without a provisioned companion column return\n * immediately. They run for system-context writes too — the projection must\n * stay correct regardless of who writes (seeds, imports, admin UI).\n */\nexport function bindSearchCompanionHooks(engine: MinimalEngine, logger?: MinimalLogger): void {\n if (typeof engine.registerHook !== 'function') return;\n if (typeof engine.unregisterHooksByPackage === 'function') {\n engine.unregisterHooksByPackage(PINYIN_SEARCH_HOOK_PACKAGE);\n }\n const opts = { packageId: PINYIN_SEARCH_HOOK_PACKAGE, priority: 150 };\n const handler = (ctx: any) => stampCompanion(engine, ctx, logger);\n engine.registerHook('beforeInsert', handler, opts);\n engine.registerHook('beforeUpdate', handler, opts);\n logger?.info?.('[pinyin-search] companion hooks bound (beforeInsert/beforeUpdate, all objects)');\n}\n\nexport interface CompanionBackfillOptions {\n /** Rows fetched per page during the scan. Default 1000. */\n batchSize?: number;\n /** Restrict to one object (reconcile entry); default: every provisioned object. */\n object?: string;\n /**\n * Recompute EVERY row's companion, not just missing ones — the periodic\n * reconcile/rebuild mode. Default false (backfill: only rows whose\n * companion is empty but whose source has CJK content).\n */\n force?: boolean;\n}\n\nexport interface CompanionBackfillResult {\n objects: number;\n scanned: number;\n updated: number;\n}\n\n/**\n * Backfill / reconcile the companion column.\n *\n * Denormalized-on-write columns go stale when writes bypass hooks (bulk\n * import, direct migration) and are empty for rows that predate the switch\n * being enabled. This scans every object that carries the companion column\n * (paged, system context) and fills the gaps; with `force: true` it\n * recomputes unconditionally (the periodic reconcile / rebuild entry).\n * Idempotent; per-row failures are skipped so one bad row never aborts the\n * pass.\n */\nexport async function backfillSearchCompanion(\n engine: MinimalEngine,\n logger?: MinimalLogger,\n options?: CompanionBackfillOptions,\n): Promise<CompanionBackfillResult> {\n const batchSize = Math.max(1, options?.batchSize ?? 1000);\n const all = options?.object\n ? [engine.registry?.getObject?.(options.object)].filter(Boolean)\n : engine.registry?.getAllObjects?.() ?? [];\n\n const result: CompanionBackfillResult = { objects: 0, scanned: 0, updated: 0 };\n\n for (const schema of all) {\n if (!schema?.name || !schema?.fields?.[SEARCH_COMPANION_FIELD]) continue;\n const sources = resolveSearchCompanionSources(schema);\n if (sources.length === 0) continue;\n result.objects++;\n\n // Seek by `id` (#4363). This walk UPDATES the rows it reads, and an offset\n // counts into a set those writes are changing, so rows slide past the\n // cursor and never get their companion blob — a backfill that reports a\n // clean pass while leaving records unsearchable.\n const walk = keysetWalk<any>(\n (q) => engine.find(schema.name, {\n ...q,\n fields: ['id', ...sources, SEARCH_COMPANION_FIELD],\n context: SYSTEM_CTX,\n }),\n { pageSize: batchSize },\n );\n\n try {\n for await (const rows of walk.pages()) {\n result.scanned += rows.length;\n\n for (const row of rows) {\n if (row?.id == null) continue;\n const hasBlob = typeof row[SEARCH_COMPANION_FIELD] === 'string' && row[SEARCH_COMPANION_FIELD] !== '';\n const hasCjkSource = sources.some((s) => containsCJK(row[s]));\n // Backfill mode: touch only rows missing a blob they should have.\n // Force mode: recompute everything (also clears stale blobs).\n if (!options?.force && (hasBlob || !hasCjkSource)) continue;\n try {\n const value = await computeSearchCompanionValue(sources.map((s) => row[s]));\n if (!options?.force && value == null) continue;\n if (value === row[SEARCH_COMPANION_FIELD]) continue;\n await engine.update(\n schema.name,\n { id: row.id, [SEARCH_COMPANION_FIELD]: value },\n { context: SYSTEM_CTX },\n );\n result.updated++;\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] backfill row skipped', {\n object: schema.name,\n id: row.id,\n error: err?.message,\n });\n }\n }\n }\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] backfill scan failed', { object: schema.name, error: err?.message });\n continue;\n }\n }\n\n if (result.updated > 0) {\n logger?.info?.('[pinyin-search] companion backfill complete', result);\n }\n return result;\n}\n\n/**\n * Periodic reconcile / rebuild entry: recompute the companion for every row\n * (optionally one object). Alias for `backfillSearchCompanion` with\n * `force: true` — exposed under its own name so operators/jobs have an\n * explicit \"rebuild the pinyin index\" handle.\n */\nexport function rebuildSearchCompanion(\n engine: MinimalEngine,\n logger?: MinimalLogger,\n options?: Omit<CompanionBackfillOptions, 'force'>,\n): Promise<CompanionBackfillResult> {\n return backfillSearchCompanion(engine, logger, { ...options, force: true });\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Pinyin normalization for the `__search` companion column (#2486).\n *\n * One normalized blob per record — full pinyin AND initials in the same\n * column — so a single `$contains` recalls every latin input shape:\n *\n * \"张伟\" → \"zhangwei zw\"\n * `zhang` / `wei` / `zhangwei` → substring of the full form\n * `zw` → substring of the initials form\n *\n * `pinyin-pro` is loaded lazily on first use: non-Chinese deployments (flag\n * off → hooks never bound) never import it and pay zero cost.\n *\n * Polyphones: pinyin-pro's default heuristics are accepted (issue #2486\n * \"待定\" — surname polyphone dictionaries are a P2 follow-up).\n */\n\nimport { containsCJK } from '@objectstack/objectql';\n\ntype PinyinFn = (text: string, options?: Record<string, unknown>) => string | string[];\n\nlet _pinyin: Promise<PinyinFn> | null = null;\n\n/** Lazy-load `pinyin-pro` (cached module-wide). */\nfunction loadPinyin(): Promise<PinyinFn> {\n _pinyin ??= import('pinyin-pro').then((m: any) => (m.pinyin ?? m.default?.pinyin) as PinyinFn);\n return _pinyin;\n}\n\n/** Lowercase and strip everything that is not a latin letter or digit. */\nfunction squash(syllables: string | string[]): string {\n const joined = Array.isArray(syllables) ? syllables.join('') : String(syllables ?? '');\n return joined.toLowerCase().replace(/[^a-z0-9]+/g, '');\n}\n\n/**\n * Compute the companion value for the given source-field values.\n *\n * Returns the normalized blob (`\"<full-pinyin> <initials>\"`, deduplicated)\n * when at least one value contains CJK characters, else `null` — a `null`\n * companion means \"nothing pinyin-searchable here\" and clears any stale blob\n * when a name is edited away from CJK. Non-CJK values need no companion:\n * their source column already matches latin input directly.\n */\nexport async function computeSearchCompanionValue(values: ReadonlyArray<unknown>): Promise<string | null> {\n const cjkValues = values.filter((v): v is string => containsCJK(v));\n if (cjkValues.length === 0) return null;\n\n const pinyin = await loadPinyin();\n const parts: string[] = [];\n for (const value of cjkValues) {\n // `nonZh: 'consecutive'` keeps latin/digit runs intact inside mixed\n // values (\"张伟2号\" → \"zhangwei2hao\"), so mixed names stay one token.\n const full = squash(pinyin(value, { toneType: 'none', type: 'array', nonZh: 'consecutive' }));\n const initials = squash(pinyin(value, { pattern: 'first', toneType: 'none', type: 'array', nonZh: 'consecutive' }));\n if (full) parts.push(full);\n if (initials && initials !== full) parts.push(initials);\n }\n if (parts.length === 0) return null;\n return [...new Set(parts)].join(' ');\n}\n"],"mappings":"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","names":["import_types","import_objectql"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/pinyin-search-plugin.ts","../src/companion-projection.ts","../src/pinyin.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { PinyinSearchPlugin } from './pinyin-search-plugin.js';\nexport type { PinyinSearchPluginOptions } from './pinyin-search-plugin.js';\nexport {\n bindSearchCompanionHooks,\n backfillSearchCompanion,\n rebuildSearchCompanion,\n PINYIN_SEARCH_HOOK_PACKAGE,\n} from './companion-projection.js';\nexport type {\n CompanionBackfillOptions,\n CompanionBackfillResult,\n} from './companion-projection.js';\nexport { computeSearchCompanionValue } from './pinyin.js';\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * PinyinSearchPlugin (#2486) — pinyin recall for `$search`.\n *\n * Pure hook plugin: the `__search` companion column is declared at object\n * compile time by the SchemaRegistry (gated on the SAME\n * `OS_SEARCH_PINYIN_ENABLED` decision point), the engine ORs it into the\n * `$search` filter, and this plugin fills the value:\n *\n * - before-save hooks: recompute full pinyin + initials of the\n * display/name field when it changes;\n * - boot backfill (`kernel:bootstrapped`): fill rows that predate the\n * switch or arrived via hook-bypassing writes;\n * - `rebuildSearchCompanion` (exported): explicit reconcile/rebuild entry.\n *\n * When the flag is off the plugin is inert: no hooks, no backfill, and\n * `pinyin-pro` is never imported.\n */\n\n// `IObjectQLEngine` via the core barrel, which re-exports it from\n// `@objectstack/spec/contracts`: this package does not depend on spec directly,\n// and the slot's contract is not a reason to add a dependency.\nimport type { IObjectQLEngine, Plugin, PluginContext } from '@objectstack/core';\nimport { resolveSearchPinyinEnabled } from '@objectstack/types';\nimport {\n bindSearchCompanionHooks,\n backfillSearchCompanion,\n} from './companion-projection.js';\n\nexport interface PinyinSearchPluginOptions {\n /**\n * Force-enable/disable regardless of `OS_SEARCH_PINYIN_ENABLED` (tests /\n * embedders). Default: `resolveSearchPinyinEnabled()`.\n */\n enabled?: boolean;\n /** Skip the boot backfill (default: run it once per boot). */\n backfill?: boolean;\n}\n\nexport class PinyinSearchPlugin implements Plugin {\n name = 'com.objectstack.plugin.pinyin-search';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: PinyinSearchPluginOptions;\n\n constructor(options: PinyinSearchPluginOptions = {}) {\n this.options = options;\n }\n\n private get enabled(): boolean {\n return this.options.enabled ?? resolveSearchPinyinEnabled();\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // Nothing to register: the companion column is provisioned by the\n // SchemaRegistry's compile-time seam, not injected at runtime.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (!this.enabled) {\n ctx.logger.debug?.('PinyinSearchPlugin: OS_SEARCH_PINYIN_ENABLED is off — inert');\n return;\n }\n\n ctx.hook('kernel:ready', async () => {\n const engine = this.resolveEngine(ctx);\n if (!engine) {\n ctx.logger.warn('PinyinSearchPlugin: no ObjectQL engine — companion hooks NOT bound');\n return;\n }\n try {\n bindSearchCompanionHooks(engine, ctx.logger as any);\n } catch (err: any) {\n ctx.logger.warn('PinyinSearchPlugin: companion hooks not bound', { error: err?.message });\n }\n });\n\n // Backfill AFTER boot settles (`kernel:bootstrapped` fires once every\n // `kernel:ready` hook — including seed loading — has completed), so\n // seeded rows written before/around hook binding are reconciled too.\n if (this.options.backfill !== false) {\n ctx.hook('kernel:bootstrapped', async () => {\n const engine = this.resolveEngine(ctx);\n if (!engine) return;\n try {\n await backfillSearchCompanion(engine, ctx.logger as any);\n } catch (err: any) {\n ctx.logger.warn('PinyinSearchPlugin: companion backfill failed', { error: err?.message });\n }\n });\n }\n }\n\n /**\n * [#4251 B5] The engine SEEN WHOLE — `bindSearchCompanionHooks` binds\n * `registerHook`, which lives on `IObjectQLEngine`, not on the data plane.\n * The ledger records `objectql` as \"the SAME instance as `data`, seen whole\",\n * so the alias fallback resolves the same object under the same contract.\n */\n private resolveEngine(ctx: PluginContext): IObjectQLEngine | null {\n try {\n return ctx.getService<IObjectQLEngine>('objectql');\n } catch {\n try {\n return ctx.getService<IObjectQLEngine>('data');\n } catch {\n return null;\n }\n }\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `__search` companion-column projection (#2486).\n *\n * The column itself is DECLARED at object compile time by the SchemaRegistry\n * (`provisionSearchCompanion`, gated on `OS_SEARCH_PINYIN_ENABLED`); this\n * module only FILLS the value — the `plugin-sharing` primary-BU projection\n * pattern (column on the object, plugin maintains it via hooks).\n *\n * Write path: global `beforeInsert`/`beforeUpdate` hooks stamp\n * `data.__search` whenever a companion source field (the object's\n * display/name field) is present in the write — i.e. only when the source\n * actually changed, avoiding write amplification. Writes that bypass hooks\n * (bulk import, direct migration) leave the companion empty; the boot\n * backfill and the `rebuildSearchCompanion` reconcile entry cover that.\n */\n\nimport {\n SEARCH_COMPANION_FIELD,\n resolveSearchCompanionSources,\n containsCJK,\n} from '@objectstack/objectql';\nimport { keysetWalk } from '@objectstack/types';\nimport { computeSearchCompanionValue } from './pinyin.js';\n\nconst SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nexport const PINYIN_SEARCH_HOOK_PACKAGE = 'plugin-pinyin-search:companion';\n\ninterface MinimalEngine {\n registerHook(\n event: string,\n handler: (ctx: any) => any | Promise<any>,\n options?: { object?: string | string[]; priority?: number; packageId?: string },\n ): void;\n unregisterHooksByPackage(packageId: string): number;\n find(object: string, query?: any, options?: any): Promise<any[]>;\n update(object: string, data: any, options?: any): Promise<any>;\n registry?: {\n getObject(name: string): any;\n getAllObjects?(packageId?: string): any[];\n };\n}\n\ninterface MinimalLogger {\n info?: (msg: any, ...rest: any[]) => void;\n warn?: (msg: any, ...rest: any[]) => void;\n debug?: (msg: any, ...rest: any[]) => void;\n}\n\n/**\n * Stamp `data.__search` on a before-save hook context when a companion source\n * field is part of the write. Recomputes from the NEW value; a non-CJK new\n * value clears the companion (null) so stale pinyin never recalls a renamed\n * record. Never throws — a normalization failure must not fail the write.\n */\nasync function stampCompanion(engine: MinimalEngine, ctx: any, logger?: MinimalLogger): Promise<void> {\n const object = ctx?.object;\n if (!object) return;\n const schema = engine.registry?.getObject?.(object);\n if (!schema?.fields?.[SEARCH_COMPANION_FIELD]) return;\n\n const data = ctx?.input?.data;\n if (!data || typeof data !== 'object' || Array.isArray(data)) return;\n\n const sources = resolveSearchCompanionSources(schema);\n if (sources.length === 0) return;\n const touched = sources.filter((s) => Object.prototype.hasOwnProperty.call(data, s));\n if (touched.length === 0) return; // source unchanged → no recompute (no write amplification)\n\n try {\n data[SEARCH_COMPANION_FIELD] = await computeSearchCompanionValue(sources.map((s) => data[s]));\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] companion normalization failed — write proceeds without it', {\n object,\n error: err?.message,\n });\n }\n}\n\n/**\n * Bind the global before-save hooks that keep `__search` in step. Idempotent\n * (unbinds the package first). Hooks are global (no object filter) with a\n * cheap early-out: objects without a provisioned companion column return\n * immediately. They run for system-context writes too — the projection must\n * stay correct regardless of who writes (seeds, imports, admin UI).\n */\nexport function bindSearchCompanionHooks(engine: MinimalEngine, logger?: MinimalLogger): void {\n if (typeof engine.registerHook !== 'function') return;\n if (typeof engine.unregisterHooksByPackage === 'function') {\n engine.unregisterHooksByPackage(PINYIN_SEARCH_HOOK_PACKAGE);\n }\n const opts = { packageId: PINYIN_SEARCH_HOOK_PACKAGE, priority: 150 };\n const handler = (ctx: any) => stampCompanion(engine, ctx, logger);\n engine.registerHook('beforeInsert', handler, opts);\n engine.registerHook('beforeUpdate', handler, opts);\n logger?.info?.('[pinyin-search] companion hooks bound (beforeInsert/beforeUpdate, all objects)');\n}\n\nexport interface CompanionBackfillOptions {\n /** Rows fetched per page during the scan. Default 1000. */\n batchSize?: number;\n /** Restrict to one object (reconcile entry); default: every provisioned object. */\n object?: string;\n /**\n * Recompute EVERY row's companion, not just missing ones — the periodic\n * reconcile/rebuild mode. Default false (backfill: only rows whose\n * companion is empty but whose source has CJK content).\n */\n force?: boolean;\n}\n\nexport interface CompanionBackfillResult {\n objects: number;\n scanned: number;\n updated: number;\n}\n\n/**\n * Backfill / reconcile the companion column.\n *\n * Denormalized-on-write columns go stale when writes bypass hooks (bulk\n * import, direct migration) and are empty for rows that predate the switch\n * being enabled. This scans every object that carries the companion column\n * (paged, system context) and fills the gaps; with `force: true` it\n * recomputes unconditionally (the periodic reconcile / rebuild entry).\n * Idempotent; per-row failures are skipped so one bad row never aborts the\n * pass.\n */\nexport async function backfillSearchCompanion(\n engine: MinimalEngine,\n logger?: MinimalLogger,\n options?: CompanionBackfillOptions,\n): Promise<CompanionBackfillResult> {\n const batchSize = Math.max(1, options?.batchSize ?? 1000);\n const all = options?.object\n ? [engine.registry?.getObject?.(options.object)].filter(Boolean)\n : engine.registry?.getAllObjects?.() ?? [];\n\n const result: CompanionBackfillResult = { objects: 0, scanned: 0, updated: 0 };\n\n for (const schema of all) {\n if (!schema?.name || !schema?.fields?.[SEARCH_COMPANION_FIELD]) continue;\n const sources = resolveSearchCompanionSources(schema);\n if (sources.length === 0) continue;\n result.objects++;\n\n // Seek by `id` (#4363). This walk UPDATES the rows it reads, and an offset\n // counts into a set those writes are changing, so rows slide past the\n // cursor and never get their companion blob — a backfill that reports a\n // clean pass while leaving records unsearchable.\n const walk = keysetWalk<any>(\n (q) => engine.find(schema.name, {\n ...q,\n fields: ['id', ...sources, SEARCH_COMPANION_FIELD],\n context: SYSTEM_CTX,\n }),\n { pageSize: batchSize },\n );\n\n try {\n for await (const rows of walk.pages()) {\n result.scanned += rows.length;\n\n for (const row of rows) {\n if (row?.id == null) continue;\n const hasBlob = typeof row[SEARCH_COMPANION_FIELD] === 'string' && row[SEARCH_COMPANION_FIELD] !== '';\n const hasCjkSource = sources.some((s) => containsCJK(row[s]));\n // Backfill mode: touch only rows missing a blob they should have.\n // Force mode: recompute everything (also clears stale blobs).\n if (!options?.force && (hasBlob || !hasCjkSource)) continue;\n try {\n const value = await computeSearchCompanionValue(sources.map((s) => row[s]));\n if (!options?.force && value == null) continue;\n if (value === row[SEARCH_COMPANION_FIELD]) continue;\n await engine.update(\n schema.name,\n { id: row.id, [SEARCH_COMPANION_FIELD]: value },\n { context: SYSTEM_CTX },\n );\n result.updated++;\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] backfill row skipped', {\n object: schema.name,\n id: row.id,\n error: err?.message,\n });\n }\n }\n }\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] backfill scan failed', { object: schema.name, error: err?.message });\n continue;\n }\n }\n\n if (result.updated > 0) {\n logger?.info?.('[pinyin-search] companion backfill complete', result);\n }\n return result;\n}\n\n/**\n * Periodic reconcile / rebuild entry: recompute the companion for every row\n * (optionally one object). Alias for `backfillSearchCompanion` with\n * `force: true` — exposed under its own name so operators/jobs have an\n * explicit \"rebuild the pinyin index\" handle.\n */\nexport function rebuildSearchCompanion(\n engine: MinimalEngine,\n logger?: MinimalLogger,\n options?: Omit<CompanionBackfillOptions, 'force'>,\n): Promise<CompanionBackfillResult> {\n return backfillSearchCompanion(engine, logger, { ...options, force: true });\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Pinyin normalization for the `__search` companion column (#2486).\n *\n * One normalized blob per record — full pinyin AND initials in the same\n * column — so a single `$contains` recalls every latin input shape:\n *\n * \"张伟\" → \"zhangwei zw\"\n * `zhang` / `wei` / `zhangwei` → substring of the full form\n * `zw` → substring of the initials form\n *\n * `pinyin-pro` is loaded lazily on first use: non-Chinese deployments (flag\n * off → hooks never bound) never import it and pay zero cost.\n *\n * Polyphones: pinyin-pro's default heuristics are accepted (issue #2486\n * \"待定\" — surname polyphone dictionaries are a P2 follow-up).\n */\n\nimport { containsCJK } from '@objectstack/objectql';\n\ntype PinyinFn = (text: string, options?: Record<string, unknown>) => string | string[];\n\nlet _pinyin: Promise<PinyinFn> | null = null;\n\n/** Lazy-load `pinyin-pro` (cached module-wide). */\nfunction loadPinyin(): Promise<PinyinFn> {\n _pinyin ??= import('pinyin-pro').then((m: any) => (m.pinyin ?? m.default?.pinyin) as PinyinFn);\n return _pinyin;\n}\n\n/** Lowercase and strip everything that is not a latin letter or digit. */\nfunction squash(syllables: string | string[]): string {\n const joined = Array.isArray(syllables) ? syllables.join('') : String(syllables ?? '');\n return joined.toLowerCase().replace(/[^a-z0-9]+/g, '');\n}\n\n/**\n * Compute the companion value for the given source-field values.\n *\n * Returns the normalized blob (`\"<full-pinyin> <initials>\"`, deduplicated)\n * when at least one value contains CJK characters, else `null` — a `null`\n * companion means \"nothing pinyin-searchable here\" and clears any stale blob\n * when a name is edited away from CJK. Non-CJK values need no companion:\n * their source column already matches latin input directly.\n */\nexport async function computeSearchCompanionValue(values: ReadonlyArray<unknown>): Promise<string | null> {\n const cjkValues = values.filter((v): v is string => containsCJK(v));\n if (cjkValues.length === 0) return null;\n\n const pinyin = await loadPinyin();\n const parts: string[] = [];\n for (const value of cjkValues) {\n // `nonZh: 'consecutive'` keeps latin/digit runs intact inside mixed\n // values (\"张伟2号\" → \"zhangwei2hao\"), so mixed names stay one token.\n const full = squash(pinyin(value, { toneType: 'none', type: 'array', nonZh: 'consecutive' }));\n const initials = squash(pinyin(value, { pattern: 'first', toneType: 'none', type: 'array', nonZh: 'consecutive' }));\n if (full) parts.push(full);\n if (initials && initials !== full) parts.push(initials);\n }\n if (parts.length === 0) return null;\n return [...new Set(parts)].join(' ');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACwBA,IAAAA,gBAA2C;;;ACN3C,IAAAC,mBAIO;AACP,mBAA2B;;;ACJ3B,sBAA4B;AAI5B,IAAI,UAAoC;AAGxC,SAAS,aAAgC;AACvC,wBAAY,OAAO,YAAY,EAAE,KAAK,CAAC,MAAY,EAAE,UAAU,EAAE,SAAS,MAAmB;AAC7F,SAAO;AACT;AAGA,SAAS,OAAO,WAAsC;AACpD,QAAM,SAAS,MAAM,QAAQ,SAAS,IAAI,UAAU,KAAK,EAAE,IAAI,OAAO,aAAa,EAAE;AACrF,SAAO,OAAO,YAAY,EAAE,QAAQ,eAAe,EAAE;AACvD;AAWA,eAAsB,4BAA4B,QAAwD;AACxG,QAAM,YAAY,OAAO,OAAO,CAAC,UAAmB,6BAAY,CAAC,CAAC;AAClE,MAAI,UAAU,WAAW,EAAG,QAAO;AAEnC,QAAM,SAAS,MAAM,WAAW;AAChC,QAAM,QAAkB,CAAC;AACzB,aAAW,SAAS,WAAW;AAG7B,UAAM,OAAO,OAAO,OAAO,OAAO,EAAE,UAAU,QAAQ,MAAM,SAAS,OAAO,cAAc,CAAC,CAAC;AAC5F,UAAM,WAAW,OAAO,OAAO,OAAO,EAAE,SAAS,SAAS,UAAU,QAAQ,MAAM,SAAS,OAAO,cAAc,CAAC,CAAC;AAClH,QAAI,KAAM,OAAM,KAAK,IAAI;AACzB,QAAI,YAAY,aAAa,KAAM,OAAM,KAAK,QAAQ;AAAA,EACxD;AACA,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,EAAE,KAAK,GAAG;AACrC;;;ADpCA,IAAM,aAAa,EAAE,UAAU,MAAM,WAAW,CAAC,GAAG,aAAa,CAAC,EAAE;AAE7D,IAAM,6BAA6B;AA6B1C,eAAe,eAAe,QAAuB,KAAU,QAAuC;AACpG,QAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ;AACb,QAAM,SAAS,OAAO,UAAU,YAAY,MAAM;AAClD,MAAI,CAAC,QAAQ,SAAS,uCAAsB,EAAG;AAE/C,QAAM,OAAO,KAAK,OAAO;AACzB,MAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,EAAG;AAE9D,QAAM,cAAU,gDAA8B,MAAM;AACpD,MAAI,QAAQ,WAAW,EAAG;AAC1B,QAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,OAAO,UAAU,eAAe,KAAK,MAAM,CAAC,CAAC;AACnF,MAAI,QAAQ,WAAW,EAAG;AAE1B,MAAI;AACF,SAAK,uCAAsB,IAAI,MAAM,4BAA4B,QAAQ,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;AAAA,EAC9F,SAAS,KAAU;AACjB,YAAQ,OAAO,mFAA8E;AAAA,MAC3F;AAAA,MACA,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AACF;AASO,SAAS,yBAAyB,QAAuB,QAA8B;AAC5F,MAAI,OAAO,OAAO,iBAAiB,WAAY;AAC/C,MAAI,OAAO,OAAO,6BAA6B,YAAY;AACzD,WAAO,yBAAyB,0BAA0B;AAAA,EAC5D;AACA,QAAM,OAAO,EAAE,WAAW,4BAA4B,UAAU,IAAI;AACpE,QAAM,UAAU,CAAC,QAAa,eAAe,QAAQ,KAAK,MAAM;AAChE,SAAO,aAAa,gBAAgB,SAAS,IAAI;AACjD,SAAO,aAAa,gBAAgB,SAAS,IAAI;AACjD,UAAQ,OAAO,gFAAgF;AACjG;AAgCA,eAAsB,wBACpB,QACA,QACA,SACkC;AAClC,QAAM,YAAY,KAAK,IAAI,GAAG,SAAS,aAAa,GAAI;AACxD,QAAM,MAAM,SAAS,SACjB,CAAC,OAAO,UAAU,YAAY,QAAQ,MAAM,CAAC,EAAE,OAAO,OAAO,IAC7D,OAAO,UAAU,gBAAgB,KAAK,CAAC;AAE3C,QAAM,SAAkC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,EAAE;AAE7E,aAAW,UAAU,KAAK;AACxB,QAAI,CAAC,QAAQ,QAAQ,CAAC,QAAQ,SAAS,uCAAsB,EAAG;AAChE,UAAM,cAAU,gDAA8B,MAAM;AACpD,QAAI,QAAQ,WAAW,EAAG;AAC1B,WAAO;AAMP,UAAM,WAAO;AAAA,MACX,CAAC,MAAM,OAAO,KAAK,OAAO,MAAM;AAAA,QAC9B,GAAG;AAAA,QACH,QAAQ,CAAC,MAAM,GAAG,SAAS,uCAAsB;AAAA,QACjD,SAAS;AAAA,MACX,CAAC;AAAA,MACD,EAAE,UAAU,UAAU;AAAA,IACxB;AAEA,QAAI;AACF,uBAAiB,QAAQ,KAAK,MAAM,GAAG;AACvC,eAAO,WAAW,KAAK;AAEvB,mBAAW,OAAO,MAAM;AACtB,cAAI,KAAK,MAAM,KAAM;AACrB,gBAAM,UAAU,OAAO,IAAI,uCAAsB,MAAM,YAAY,IAAI,uCAAsB,MAAM;AACnG,gBAAM,eAAe,QAAQ,KAAK,CAAC,UAAM,8BAAY,IAAI,CAAC,CAAC,CAAC;AAG5D,cAAI,CAAC,SAAS,UAAU,WAAW,CAAC,cAAe;AACnD,cAAI;AACF,kBAAM,QAAQ,MAAM,4BAA4B,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;AAC1E,gBAAI,CAAC,SAAS,SAAS,SAAS,KAAM;AACtC,gBAAI,UAAU,IAAI,uCAAsB,EAAG;AAC3C,kBAAM,OAAO;AAAA,cACX,OAAO;AAAA,cACP,EAAE,IAAI,IAAI,IAAI,CAAC,uCAAsB,GAAG,MAAM;AAAA,cAC9C,EAAE,SAAS,WAAW;AAAA,YACxB;AACA,mBAAO;AAAA,UACT,SAAS,KAAU;AACjB,oBAAQ,OAAO,wCAAwC;AAAA,cACrD,QAAQ,OAAO;AAAA,cACf,IAAI,IAAI;AAAA,cACR,OAAO,KAAK;AAAA,YACd,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACA;AAAA,IACF,SAAS,KAAU;AACjB,cAAQ,OAAO,wCAAwC,EAAE,QAAQ,OAAO,MAAM,OAAO,KAAK,QAAQ,CAAC;AACnG;AAAA,IACF;AAAA,EACF;AAEA,MAAI,OAAO,UAAU,GAAG;AACtB,YAAQ,OAAO,+CAA+C,MAAM;AAAA,EACtE;AACA,SAAO;AACT;AAQO,SAAS,uBACd,QACA,QACA,SACkC;AAClC,SAAO,wBAAwB,QAAQ,QAAQ,EAAE,GAAG,SAAS,OAAO,KAAK,CAAC;AAC5E;;;AD/KO,IAAM,qBAAN,MAA2C;AAAA,EAQhD,YAAY,UAAqC,CAAC,GAAG;AAPrD,gBAAO;AACP,mBAAU;AACV,gBAAO;AACP,wBAAe,CAAC,iCAAiC;AAK/C,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,IAAY,UAAmB;AAC7B,WAAO,KAAK,QAAQ,eAAW,0CAA2B;AAAA,EAC5D;AAAA,EAEA,MAAM,KAAK,MAAoC;AAAA,EAG/C;AAAA,EAEA,MAAM,MAAM,KAAmC;AAC7C,QAAI,CAAC,KAAK,SAAS;AACjB,UAAI,OAAO,QAAQ,kEAA6D;AAChF;AAAA,IACF;AAEA,QAAI,KAAK,gBAAgB,YAAY;AACnC,YAAM,SAAS,KAAK,cAAc,GAAG;AACrC,UAAI,CAAC,QAAQ;AACX,YAAI,OAAO,KAAK,yEAAoE;AACpF;AAAA,MACF;AACA,UAAI;AACF,iCAAyB,QAAQ,IAAI,MAAa;AAAA,MACpD,SAAS,KAAU;AACjB,YAAI,OAAO,KAAK,iDAAiD,EAAE,OAAO,KAAK,QAAQ,CAAC;AAAA,MAC1F;AAAA,IACF,CAAC;AAKD,QAAI,KAAK,QAAQ,aAAa,OAAO;AACnC,UAAI,KAAK,uBAAuB,YAAY;AAC1C,cAAM,SAAS,KAAK,cAAc,GAAG;AACrC,YAAI,CAAC,OAAQ;AACb,YAAI;AACF,gBAAM,wBAAwB,QAAQ,IAAI,MAAa;AAAA,QACzD,SAAS,KAAU;AACjB,cAAI,OAAO,KAAK,iDAAiD,EAAE,OAAO,KAAK,QAAQ,CAAC;AAAA,QAC1F;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,cAAc,KAA4C;AAChE,QAAI;AACF,aAAO,IAAI,WAA4B,UAAU;AAAA,IACnD,QAAQ;AACN,UAAI;AACF,eAAO,IAAI,WAA4B,MAAM;AAAA,MAC/C,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;","names":["import_types","import_objectql"]}
|
package/dist/index.mjs
CHANGED
|
@@ -170,6 +170,12 @@ var PinyinSearchPlugin = class {
|
|
|
170
170
|
});
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* [#4251 B5] The engine SEEN WHOLE — `bindSearchCompanionHooks` binds
|
|
175
|
+
* `registerHook`, which lives on `IObjectQLEngine`, not on the data plane.
|
|
176
|
+
* The ledger records `objectql` as "the SAME instance as `data`, seen whole",
|
|
177
|
+
* so the alias fallback resolves the same object under the same contract.
|
|
178
|
+
*/
|
|
173
179
|
resolveEngine(ctx) {
|
|
174
180
|
try {
|
|
175
181
|
return ctx.getService("objectql");
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/pinyin-search-plugin.ts","../src/companion-projection.ts","../src/pinyin.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * PinyinSearchPlugin (#2486) — pinyin recall for `$search`.\n *\n * Pure hook plugin: the `__search` companion column is declared at object\n * compile time by the SchemaRegistry (gated on the SAME\n * `OS_SEARCH_PINYIN_ENABLED` decision point), the engine ORs it into the\n * `$search` filter, and this plugin fills the value:\n *\n * - before-save hooks: recompute full pinyin + initials of the\n * display/name field when it changes;\n * - boot backfill (`kernel:bootstrapped`): fill rows that predate the\n * switch or arrived via hook-bypassing writes;\n * - `rebuildSearchCompanion` (exported): explicit reconcile/rebuild entry.\n *\n * When the flag is off the plugin is inert: no hooks, no backfill, and\n * `pinyin-pro` is never imported.\n */\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { resolveSearchPinyinEnabled } from '@objectstack/types';\nimport {\n bindSearchCompanionHooks,\n backfillSearchCompanion,\n} from './companion-projection.js';\n\nexport interface PinyinSearchPluginOptions {\n /**\n * Force-enable/disable regardless of `OS_SEARCH_PINYIN_ENABLED` (tests /\n * embedders). Default: `resolveSearchPinyinEnabled()`.\n */\n enabled?: boolean;\n /** Skip the boot backfill (default: run it once per boot). */\n backfill?: boolean;\n}\n\nexport class PinyinSearchPlugin implements Plugin {\n name = 'com.objectstack.plugin.pinyin-search';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: PinyinSearchPluginOptions;\n\n constructor(options: PinyinSearchPluginOptions = {}) {\n this.options = options;\n }\n\n private get enabled(): boolean {\n return this.options.enabled ?? resolveSearchPinyinEnabled();\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // Nothing to register: the companion column is provisioned by the\n // SchemaRegistry's compile-time seam, not injected at runtime.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (!this.enabled) {\n ctx.logger.debug?.('PinyinSearchPlugin: OS_SEARCH_PINYIN_ENABLED is off — inert');\n return;\n }\n\n ctx.hook('kernel:ready', async () => {\n const engine = this.resolveEngine(ctx);\n if (!engine) {\n ctx.logger.warn('PinyinSearchPlugin: no ObjectQL engine — companion hooks NOT bound');\n return;\n }\n try {\n bindSearchCompanionHooks(engine, ctx.logger as any);\n } catch (err: any) {\n ctx.logger.warn('PinyinSearchPlugin: companion hooks not bound', { error: err?.message });\n }\n });\n\n // Backfill AFTER boot settles (`kernel:bootstrapped` fires once every\n // `kernel:ready` hook — including seed loading — has completed), so\n // seeded rows written before/around hook binding are reconciled too.\n if (this.options.backfill !== false) {\n ctx.hook('kernel:bootstrapped', async () => {\n const engine = this.resolveEngine(ctx);\n if (!engine) return;\n try {\n await backfillSearchCompanion(engine, ctx.logger as any);\n } catch (err: any) {\n ctx.logger.warn('PinyinSearchPlugin: companion backfill failed', { error: err?.message });\n }\n });\n }\n }\n\n private resolveEngine(ctx: PluginContext): any {\n try {\n return ctx.getService<any>('objectql');\n } catch {\n try {\n return ctx.getService<any>('data');\n } catch {\n return null;\n }\n }\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `__search` companion-column projection (#2486).\n *\n * The column itself is DECLARED at object compile time by the SchemaRegistry\n * (`provisionSearchCompanion`, gated on `OS_SEARCH_PINYIN_ENABLED`); this\n * module only FILLS the value — the `plugin-sharing` primary-BU projection\n * pattern (column on the object, plugin maintains it via hooks).\n *\n * Write path: global `beforeInsert`/`beforeUpdate` hooks stamp\n * `data.__search` whenever a companion source field (the object's\n * display/name field) is present in the write — i.e. only when the source\n * actually changed, avoiding write amplification. Writes that bypass hooks\n * (bulk import, direct migration) leave the companion empty; the boot\n * backfill and the `rebuildSearchCompanion` reconcile entry cover that.\n */\n\nimport {\n SEARCH_COMPANION_FIELD,\n resolveSearchCompanionSources,\n containsCJK,\n} from '@objectstack/objectql';\nimport { keysetWalk } from '@objectstack/types';\nimport { computeSearchCompanionValue } from './pinyin.js';\n\nconst SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nexport const PINYIN_SEARCH_HOOK_PACKAGE = 'plugin-pinyin-search:companion';\n\ninterface MinimalEngine {\n registerHook(\n event: string,\n handler: (ctx: any) => any | Promise<any>,\n options?: { object?: string | string[]; priority?: number; packageId?: string },\n ): void;\n unregisterHooksByPackage(packageId: string): number;\n find(object: string, query?: any, options?: any): Promise<any[]>;\n update(object: string, data: any, options?: any): Promise<any>;\n registry?: {\n getObject(name: string): any;\n getAllObjects?(packageId?: string): any[];\n };\n}\n\ninterface MinimalLogger {\n info?: (msg: any, ...rest: any[]) => void;\n warn?: (msg: any, ...rest: any[]) => void;\n debug?: (msg: any, ...rest: any[]) => void;\n}\n\n/**\n * Stamp `data.__search` on a before-save hook context when a companion source\n * field is part of the write. Recomputes from the NEW value; a non-CJK new\n * value clears the companion (null) so stale pinyin never recalls a renamed\n * record. Never throws — a normalization failure must not fail the write.\n */\nasync function stampCompanion(engine: MinimalEngine, ctx: any, logger?: MinimalLogger): Promise<void> {\n const object = ctx?.object;\n if (!object) return;\n const schema = engine.registry?.getObject?.(object);\n if (!schema?.fields?.[SEARCH_COMPANION_FIELD]) return;\n\n const data = ctx?.input?.data;\n if (!data || typeof data !== 'object' || Array.isArray(data)) return;\n\n const sources = resolveSearchCompanionSources(schema);\n if (sources.length === 0) return;\n const touched = sources.filter((s) => Object.prototype.hasOwnProperty.call(data, s));\n if (touched.length === 0) return; // source unchanged → no recompute (no write amplification)\n\n try {\n data[SEARCH_COMPANION_FIELD] = await computeSearchCompanionValue(sources.map((s) => data[s]));\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] companion normalization failed — write proceeds without it', {\n object,\n error: err?.message,\n });\n }\n}\n\n/**\n * Bind the global before-save hooks that keep `__search` in step. Idempotent\n * (unbinds the package first). Hooks are global (no object filter) with a\n * cheap early-out: objects without a provisioned companion column return\n * immediately. They run for system-context writes too — the projection must\n * stay correct regardless of who writes (seeds, imports, admin UI).\n */\nexport function bindSearchCompanionHooks(engine: MinimalEngine, logger?: MinimalLogger): void {\n if (typeof engine.registerHook !== 'function') return;\n if (typeof engine.unregisterHooksByPackage === 'function') {\n engine.unregisterHooksByPackage(PINYIN_SEARCH_HOOK_PACKAGE);\n }\n const opts = { packageId: PINYIN_SEARCH_HOOK_PACKAGE, priority: 150 };\n const handler = (ctx: any) => stampCompanion(engine, ctx, logger);\n engine.registerHook('beforeInsert', handler, opts);\n engine.registerHook('beforeUpdate', handler, opts);\n logger?.info?.('[pinyin-search] companion hooks bound (beforeInsert/beforeUpdate, all objects)');\n}\n\nexport interface CompanionBackfillOptions {\n /** Rows fetched per page during the scan. Default 1000. */\n batchSize?: number;\n /** Restrict to one object (reconcile entry); default: every provisioned object. */\n object?: string;\n /**\n * Recompute EVERY row's companion, not just missing ones — the periodic\n * reconcile/rebuild mode. Default false (backfill: only rows whose\n * companion is empty but whose source has CJK content).\n */\n force?: boolean;\n}\n\nexport interface CompanionBackfillResult {\n objects: number;\n scanned: number;\n updated: number;\n}\n\n/**\n * Backfill / reconcile the companion column.\n *\n * Denormalized-on-write columns go stale when writes bypass hooks (bulk\n * import, direct migration) and are empty for rows that predate the switch\n * being enabled. This scans every object that carries the companion column\n * (paged, system context) and fills the gaps; with `force: true` it\n * recomputes unconditionally (the periodic reconcile / rebuild entry).\n * Idempotent; per-row failures are skipped so one bad row never aborts the\n * pass.\n */\nexport async function backfillSearchCompanion(\n engine: MinimalEngine,\n logger?: MinimalLogger,\n options?: CompanionBackfillOptions,\n): Promise<CompanionBackfillResult> {\n const batchSize = Math.max(1, options?.batchSize ?? 1000);\n const all = options?.object\n ? [engine.registry?.getObject?.(options.object)].filter(Boolean)\n : engine.registry?.getAllObjects?.() ?? [];\n\n const result: CompanionBackfillResult = { objects: 0, scanned: 0, updated: 0 };\n\n for (const schema of all) {\n if (!schema?.name || !schema?.fields?.[SEARCH_COMPANION_FIELD]) continue;\n const sources = resolveSearchCompanionSources(schema);\n if (sources.length === 0) continue;\n result.objects++;\n\n // Seek by `id` (#4363). This walk UPDATES the rows it reads, and an offset\n // counts into a set those writes are changing, so rows slide past the\n // cursor and never get their companion blob — a backfill that reports a\n // clean pass while leaving records unsearchable.\n const walk = keysetWalk<any>(\n (q) => engine.find(schema.name, {\n ...q,\n fields: ['id', ...sources, SEARCH_COMPANION_FIELD],\n context: SYSTEM_CTX,\n }),\n { pageSize: batchSize },\n );\n\n try {\n for await (const rows of walk.pages()) {\n result.scanned += rows.length;\n\n for (const row of rows) {\n if (row?.id == null) continue;\n const hasBlob = typeof row[SEARCH_COMPANION_FIELD] === 'string' && row[SEARCH_COMPANION_FIELD] !== '';\n const hasCjkSource = sources.some((s) => containsCJK(row[s]));\n // Backfill mode: touch only rows missing a blob they should have.\n // Force mode: recompute everything (also clears stale blobs).\n if (!options?.force && (hasBlob || !hasCjkSource)) continue;\n try {\n const value = await computeSearchCompanionValue(sources.map((s) => row[s]));\n if (!options?.force && value == null) continue;\n if (value === row[SEARCH_COMPANION_FIELD]) continue;\n await engine.update(\n schema.name,\n { id: row.id, [SEARCH_COMPANION_FIELD]: value },\n { context: SYSTEM_CTX },\n );\n result.updated++;\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] backfill row skipped', {\n object: schema.name,\n id: row.id,\n error: err?.message,\n });\n }\n }\n }\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] backfill scan failed', { object: schema.name, error: err?.message });\n continue;\n }\n }\n\n if (result.updated > 0) {\n logger?.info?.('[pinyin-search] companion backfill complete', result);\n }\n return result;\n}\n\n/**\n * Periodic reconcile / rebuild entry: recompute the companion for every row\n * (optionally one object). Alias for `backfillSearchCompanion` with\n * `force: true` — exposed under its own name so operators/jobs have an\n * explicit \"rebuild the pinyin index\" handle.\n */\nexport function rebuildSearchCompanion(\n engine: MinimalEngine,\n logger?: MinimalLogger,\n options?: Omit<CompanionBackfillOptions, 'force'>,\n): Promise<CompanionBackfillResult> {\n return backfillSearchCompanion(engine, logger, { ...options, force: true });\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Pinyin normalization for the `__search` companion column (#2486).\n *\n * One normalized blob per record — full pinyin AND initials in the same\n * column — so a single `$contains` recalls every latin input shape:\n *\n * \"张伟\" → \"zhangwei zw\"\n * `zhang` / `wei` / `zhangwei` → substring of the full form\n * `zw` → substring of the initials form\n *\n * `pinyin-pro` is loaded lazily on first use: non-Chinese deployments (flag\n * off → hooks never bound) never import it and pay zero cost.\n *\n * Polyphones: pinyin-pro's default heuristics are accepted (issue #2486\n * \"待定\" — surname polyphone dictionaries are a P2 follow-up).\n */\n\nimport { containsCJK } from '@objectstack/objectql';\n\ntype PinyinFn = (text: string, options?: Record<string, unknown>) => string | string[];\n\nlet _pinyin: Promise<PinyinFn> | null = null;\n\n/** Lazy-load `pinyin-pro` (cached module-wide). */\nfunction loadPinyin(): Promise<PinyinFn> {\n _pinyin ??= import('pinyin-pro').then((m: any) => (m.pinyin ?? m.default?.pinyin) as PinyinFn);\n return _pinyin;\n}\n\n/** Lowercase and strip everything that is not a latin letter or digit. */\nfunction squash(syllables: string | string[]): string {\n const joined = Array.isArray(syllables) ? syllables.join('') : String(syllables ?? '');\n return joined.toLowerCase().replace(/[^a-z0-9]+/g, '');\n}\n\n/**\n * Compute the companion value for the given source-field values.\n *\n * Returns the normalized blob (`\"<full-pinyin> <initials>\"`, deduplicated)\n * when at least one value contains CJK characters, else `null` — a `null`\n * companion means \"nothing pinyin-searchable here\" and clears any stale blob\n * when a name is edited away from CJK. Non-CJK values need no companion:\n * their source column already matches latin input directly.\n */\nexport async function computeSearchCompanionValue(values: ReadonlyArray<unknown>): Promise<string | null> {\n const cjkValues = values.filter((v): v is string => containsCJK(v));\n if (cjkValues.length === 0) return null;\n\n const pinyin = await loadPinyin();\n const parts: string[] = [];\n for (const value of cjkValues) {\n // `nonZh: 'consecutive'` keeps latin/digit runs intact inside mixed\n // values (\"张伟2号\" → \"zhangwei2hao\"), so mixed names stay one token.\n const full = squash(pinyin(value, { toneType: 'none', type: 'array', nonZh: 'consecutive' }));\n const initials = squash(pinyin(value, { pattern: 'first', toneType: 'none', type: 'array', nonZh: 'consecutive' }));\n if (full) parts.push(full);\n if (initials && initials !== full) parts.push(initials);\n }\n if (parts.length === 0) return null;\n return [...new Set(parts)].join(' ');\n}\n"],"mappings":"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","names":["containsCJK","containsCJK"]}
|
|
1
|
+
{"version":3,"sources":["../src/pinyin-search-plugin.ts","../src/companion-projection.ts","../src/pinyin.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * PinyinSearchPlugin (#2486) — pinyin recall for `$search`.\n *\n * Pure hook plugin: the `__search` companion column is declared at object\n * compile time by the SchemaRegistry (gated on the SAME\n * `OS_SEARCH_PINYIN_ENABLED` decision point), the engine ORs it into the\n * `$search` filter, and this plugin fills the value:\n *\n * - before-save hooks: recompute full pinyin + initials of the\n * display/name field when it changes;\n * - boot backfill (`kernel:bootstrapped`): fill rows that predate the\n * switch or arrived via hook-bypassing writes;\n * - `rebuildSearchCompanion` (exported): explicit reconcile/rebuild entry.\n *\n * When the flag is off the plugin is inert: no hooks, no backfill, and\n * `pinyin-pro` is never imported.\n */\n\n// `IObjectQLEngine` via the core barrel, which re-exports it from\n// `@objectstack/spec/contracts`: this package does not depend on spec directly,\n// and the slot's contract is not a reason to add a dependency.\nimport type { IObjectQLEngine, Plugin, PluginContext } from '@objectstack/core';\nimport { resolveSearchPinyinEnabled } from '@objectstack/types';\nimport {\n bindSearchCompanionHooks,\n backfillSearchCompanion,\n} from './companion-projection.js';\n\nexport interface PinyinSearchPluginOptions {\n /**\n * Force-enable/disable regardless of `OS_SEARCH_PINYIN_ENABLED` (tests /\n * embedders). Default: `resolveSearchPinyinEnabled()`.\n */\n enabled?: boolean;\n /** Skip the boot backfill (default: run it once per boot). */\n backfill?: boolean;\n}\n\nexport class PinyinSearchPlugin implements Plugin {\n name = 'com.objectstack.plugin.pinyin-search';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: PinyinSearchPluginOptions;\n\n constructor(options: PinyinSearchPluginOptions = {}) {\n this.options = options;\n }\n\n private get enabled(): boolean {\n return this.options.enabled ?? resolveSearchPinyinEnabled();\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // Nothing to register: the companion column is provisioned by the\n // SchemaRegistry's compile-time seam, not injected at runtime.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (!this.enabled) {\n ctx.logger.debug?.('PinyinSearchPlugin: OS_SEARCH_PINYIN_ENABLED is off — inert');\n return;\n }\n\n ctx.hook('kernel:ready', async () => {\n const engine = this.resolveEngine(ctx);\n if (!engine) {\n ctx.logger.warn('PinyinSearchPlugin: no ObjectQL engine — companion hooks NOT bound');\n return;\n }\n try {\n bindSearchCompanionHooks(engine, ctx.logger as any);\n } catch (err: any) {\n ctx.logger.warn('PinyinSearchPlugin: companion hooks not bound', { error: err?.message });\n }\n });\n\n // Backfill AFTER boot settles (`kernel:bootstrapped` fires once every\n // `kernel:ready` hook — including seed loading — has completed), so\n // seeded rows written before/around hook binding are reconciled too.\n if (this.options.backfill !== false) {\n ctx.hook('kernel:bootstrapped', async () => {\n const engine = this.resolveEngine(ctx);\n if (!engine) return;\n try {\n await backfillSearchCompanion(engine, ctx.logger as any);\n } catch (err: any) {\n ctx.logger.warn('PinyinSearchPlugin: companion backfill failed', { error: err?.message });\n }\n });\n }\n }\n\n /**\n * [#4251 B5] The engine SEEN WHOLE — `bindSearchCompanionHooks` binds\n * `registerHook`, which lives on `IObjectQLEngine`, not on the data plane.\n * The ledger records `objectql` as \"the SAME instance as `data`, seen whole\",\n * so the alias fallback resolves the same object under the same contract.\n */\n private resolveEngine(ctx: PluginContext): IObjectQLEngine | null {\n try {\n return ctx.getService<IObjectQLEngine>('objectql');\n } catch {\n try {\n return ctx.getService<IObjectQLEngine>('data');\n } catch {\n return null;\n }\n }\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `__search` companion-column projection (#2486).\n *\n * The column itself is DECLARED at object compile time by the SchemaRegistry\n * (`provisionSearchCompanion`, gated on `OS_SEARCH_PINYIN_ENABLED`); this\n * module only FILLS the value — the `plugin-sharing` primary-BU projection\n * pattern (column on the object, plugin maintains it via hooks).\n *\n * Write path: global `beforeInsert`/`beforeUpdate` hooks stamp\n * `data.__search` whenever a companion source field (the object's\n * display/name field) is present in the write — i.e. only when the source\n * actually changed, avoiding write amplification. Writes that bypass hooks\n * (bulk import, direct migration) leave the companion empty; the boot\n * backfill and the `rebuildSearchCompanion` reconcile entry cover that.\n */\n\nimport {\n SEARCH_COMPANION_FIELD,\n resolveSearchCompanionSources,\n containsCJK,\n} from '@objectstack/objectql';\nimport { keysetWalk } from '@objectstack/types';\nimport { computeSearchCompanionValue } from './pinyin.js';\n\nconst SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nexport const PINYIN_SEARCH_HOOK_PACKAGE = 'plugin-pinyin-search:companion';\n\ninterface MinimalEngine {\n registerHook(\n event: string,\n handler: (ctx: any) => any | Promise<any>,\n options?: { object?: string | string[]; priority?: number; packageId?: string },\n ): void;\n unregisterHooksByPackage(packageId: string): number;\n find(object: string, query?: any, options?: any): Promise<any[]>;\n update(object: string, data: any, options?: any): Promise<any>;\n registry?: {\n getObject(name: string): any;\n getAllObjects?(packageId?: string): any[];\n };\n}\n\ninterface MinimalLogger {\n info?: (msg: any, ...rest: any[]) => void;\n warn?: (msg: any, ...rest: any[]) => void;\n debug?: (msg: any, ...rest: any[]) => void;\n}\n\n/**\n * Stamp `data.__search` on a before-save hook context when a companion source\n * field is part of the write. Recomputes from the NEW value; a non-CJK new\n * value clears the companion (null) so stale pinyin never recalls a renamed\n * record. Never throws — a normalization failure must not fail the write.\n */\nasync function stampCompanion(engine: MinimalEngine, ctx: any, logger?: MinimalLogger): Promise<void> {\n const object = ctx?.object;\n if (!object) return;\n const schema = engine.registry?.getObject?.(object);\n if (!schema?.fields?.[SEARCH_COMPANION_FIELD]) return;\n\n const data = ctx?.input?.data;\n if (!data || typeof data !== 'object' || Array.isArray(data)) return;\n\n const sources = resolveSearchCompanionSources(schema);\n if (sources.length === 0) return;\n const touched = sources.filter((s) => Object.prototype.hasOwnProperty.call(data, s));\n if (touched.length === 0) return; // source unchanged → no recompute (no write amplification)\n\n try {\n data[SEARCH_COMPANION_FIELD] = await computeSearchCompanionValue(sources.map((s) => data[s]));\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] companion normalization failed — write proceeds without it', {\n object,\n error: err?.message,\n });\n }\n}\n\n/**\n * Bind the global before-save hooks that keep `__search` in step. Idempotent\n * (unbinds the package first). Hooks are global (no object filter) with a\n * cheap early-out: objects without a provisioned companion column return\n * immediately. They run for system-context writes too — the projection must\n * stay correct regardless of who writes (seeds, imports, admin UI).\n */\nexport function bindSearchCompanionHooks(engine: MinimalEngine, logger?: MinimalLogger): void {\n if (typeof engine.registerHook !== 'function') return;\n if (typeof engine.unregisterHooksByPackage === 'function') {\n engine.unregisterHooksByPackage(PINYIN_SEARCH_HOOK_PACKAGE);\n }\n const opts = { packageId: PINYIN_SEARCH_HOOK_PACKAGE, priority: 150 };\n const handler = (ctx: any) => stampCompanion(engine, ctx, logger);\n engine.registerHook('beforeInsert', handler, opts);\n engine.registerHook('beforeUpdate', handler, opts);\n logger?.info?.('[pinyin-search] companion hooks bound (beforeInsert/beforeUpdate, all objects)');\n}\n\nexport interface CompanionBackfillOptions {\n /** Rows fetched per page during the scan. Default 1000. */\n batchSize?: number;\n /** Restrict to one object (reconcile entry); default: every provisioned object. */\n object?: string;\n /**\n * Recompute EVERY row's companion, not just missing ones — the periodic\n * reconcile/rebuild mode. Default false (backfill: only rows whose\n * companion is empty but whose source has CJK content).\n */\n force?: boolean;\n}\n\nexport interface CompanionBackfillResult {\n objects: number;\n scanned: number;\n updated: number;\n}\n\n/**\n * Backfill / reconcile the companion column.\n *\n * Denormalized-on-write columns go stale when writes bypass hooks (bulk\n * import, direct migration) and are empty for rows that predate the switch\n * being enabled. This scans every object that carries the companion column\n * (paged, system context) and fills the gaps; with `force: true` it\n * recomputes unconditionally (the periodic reconcile / rebuild entry).\n * Idempotent; per-row failures are skipped so one bad row never aborts the\n * pass.\n */\nexport async function backfillSearchCompanion(\n engine: MinimalEngine,\n logger?: MinimalLogger,\n options?: CompanionBackfillOptions,\n): Promise<CompanionBackfillResult> {\n const batchSize = Math.max(1, options?.batchSize ?? 1000);\n const all = options?.object\n ? [engine.registry?.getObject?.(options.object)].filter(Boolean)\n : engine.registry?.getAllObjects?.() ?? [];\n\n const result: CompanionBackfillResult = { objects: 0, scanned: 0, updated: 0 };\n\n for (const schema of all) {\n if (!schema?.name || !schema?.fields?.[SEARCH_COMPANION_FIELD]) continue;\n const sources = resolveSearchCompanionSources(schema);\n if (sources.length === 0) continue;\n result.objects++;\n\n // Seek by `id` (#4363). This walk UPDATES the rows it reads, and an offset\n // counts into a set those writes are changing, so rows slide past the\n // cursor and never get their companion blob — a backfill that reports a\n // clean pass while leaving records unsearchable.\n const walk = keysetWalk<any>(\n (q) => engine.find(schema.name, {\n ...q,\n fields: ['id', ...sources, SEARCH_COMPANION_FIELD],\n context: SYSTEM_CTX,\n }),\n { pageSize: batchSize },\n );\n\n try {\n for await (const rows of walk.pages()) {\n result.scanned += rows.length;\n\n for (const row of rows) {\n if (row?.id == null) continue;\n const hasBlob = typeof row[SEARCH_COMPANION_FIELD] === 'string' && row[SEARCH_COMPANION_FIELD] !== '';\n const hasCjkSource = sources.some((s) => containsCJK(row[s]));\n // Backfill mode: touch only rows missing a blob they should have.\n // Force mode: recompute everything (also clears stale blobs).\n if (!options?.force && (hasBlob || !hasCjkSource)) continue;\n try {\n const value = await computeSearchCompanionValue(sources.map((s) => row[s]));\n if (!options?.force && value == null) continue;\n if (value === row[SEARCH_COMPANION_FIELD]) continue;\n await engine.update(\n schema.name,\n { id: row.id, [SEARCH_COMPANION_FIELD]: value },\n { context: SYSTEM_CTX },\n );\n result.updated++;\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] backfill row skipped', {\n object: schema.name,\n id: row.id,\n error: err?.message,\n });\n }\n }\n }\n } catch (err: any) {\n logger?.warn?.('[pinyin-search] backfill scan failed', { object: schema.name, error: err?.message });\n continue;\n }\n }\n\n if (result.updated > 0) {\n logger?.info?.('[pinyin-search] companion backfill complete', result);\n }\n return result;\n}\n\n/**\n * Periodic reconcile / rebuild entry: recompute the companion for every row\n * (optionally one object). Alias for `backfillSearchCompanion` with\n * `force: true` — exposed under its own name so operators/jobs have an\n * explicit \"rebuild the pinyin index\" handle.\n */\nexport function rebuildSearchCompanion(\n engine: MinimalEngine,\n logger?: MinimalLogger,\n options?: Omit<CompanionBackfillOptions, 'force'>,\n): Promise<CompanionBackfillResult> {\n return backfillSearchCompanion(engine, logger, { ...options, force: true });\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Pinyin normalization for the `__search` companion column (#2486).\n *\n * One normalized blob per record — full pinyin AND initials in the same\n * column — so a single `$contains` recalls every latin input shape:\n *\n * \"张伟\" → \"zhangwei zw\"\n * `zhang` / `wei` / `zhangwei` → substring of the full form\n * `zw` → substring of the initials form\n *\n * `pinyin-pro` is loaded lazily on first use: non-Chinese deployments (flag\n * off → hooks never bound) never import it and pay zero cost.\n *\n * Polyphones: pinyin-pro's default heuristics are accepted (issue #2486\n * \"待定\" — surname polyphone dictionaries are a P2 follow-up).\n */\n\nimport { containsCJK } from '@objectstack/objectql';\n\ntype PinyinFn = (text: string, options?: Record<string, unknown>) => string | string[];\n\nlet _pinyin: Promise<PinyinFn> | null = null;\n\n/** Lazy-load `pinyin-pro` (cached module-wide). */\nfunction loadPinyin(): Promise<PinyinFn> {\n _pinyin ??= import('pinyin-pro').then((m: any) => (m.pinyin ?? m.default?.pinyin) as PinyinFn);\n return _pinyin;\n}\n\n/** Lowercase and strip everything that is not a latin letter or digit. */\nfunction squash(syllables: string | string[]): string {\n const joined = Array.isArray(syllables) ? syllables.join('') : String(syllables ?? '');\n return joined.toLowerCase().replace(/[^a-z0-9]+/g, '');\n}\n\n/**\n * Compute the companion value for the given source-field values.\n *\n * Returns the normalized blob (`\"<full-pinyin> <initials>\"`, deduplicated)\n * when at least one value contains CJK characters, else `null` — a `null`\n * companion means \"nothing pinyin-searchable here\" and clears any stale blob\n * when a name is edited away from CJK. Non-CJK values need no companion:\n * their source column already matches latin input directly.\n */\nexport async function computeSearchCompanionValue(values: ReadonlyArray<unknown>): Promise<string | null> {\n const cjkValues = values.filter((v): v is string => containsCJK(v));\n if (cjkValues.length === 0) return null;\n\n const pinyin = await loadPinyin();\n const parts: string[] = [];\n for (const value of cjkValues) {\n // `nonZh: 'consecutive'` keeps latin/digit runs intact inside mixed\n // values (\"张伟2号\" → \"zhangwei2hao\"), so mixed names stay one token.\n const full = squash(pinyin(value, { toneType: 'none', type: 'array', nonZh: 'consecutive' }));\n const initials = squash(pinyin(value, { pattern: 'first', toneType: 'none', type: 'array', nonZh: 'consecutive' }));\n if (full) parts.push(full);\n if (initials && initials !== full) parts.push(initials);\n }\n if (parts.length === 0) return null;\n return [...new Set(parts)].join(' ');\n}\n"],"mappings":"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","names":["containsCJK","containsCJK"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/plugin-pinyin-search",
|
|
3
|
-
"version": "17.0.0
|
|
3
|
+
"version": "17.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Pinyin search recall for ObjectStack — populates the hidden `__search` companion column (full pinyin + initials of the display/name field) so `$search` hits CJK names typed as pinyin. Locale-gated via OS_SEARCH_PINYIN_ENABLED (#2486).",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"pinyin-pro": "^3.28.2",
|
|
17
|
-
"@objectstack/core": "17.0.0
|
|
18
|
-
"@objectstack/objectql": "17.0.0
|
|
19
|
-
"@objectstack/types": "17.0.0
|
|
17
|
+
"@objectstack/core": "17.0.0",
|
|
18
|
+
"@objectstack/objectql": "17.0.0",
|
|
19
|
+
"@objectstack/types": "17.0.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^26.1.2",
|