@nitra/cursor 1.29.4 → 1.30.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 CHANGED
@@ -3,6 +3,20 @@
3
3
  Усі помітні зміни цього модуля документуються тут.
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
+ # [1.30.0] - 2026-05-29
7
+
8
+ ### Changed
9
+
10
+ - **`rules/ci4/ci4.mdc`** (`version` 2.1 → 3.0) — viewer-story повністю переписано під **Zed + marksman LSP без site-generator-а**. MkDocs Material і pymdownx-розширення (`!!! note`, `??? engineer`, `=== "tab"`, кастомні audience-CSS) прибрані як рекомендація: їх не рендерить вбудований MD-preview Zed, що ламає принцип «відкрив файл = бачу фінальний вигляд». Collapsible-блоки для змішаної аудиторії — через нативний HTML5 `<details>` / `<summary>`, що рендериться скрізь (Zed preview, GitHub, будь-який майбутній збирач) без розширень парсера. Введено **portable-only subset**: CommonMark + GFM + Mermaid у fenced code + KaTeX + `<details>`; заборонені VitePress containers (`::: tip`), MDX/Astro-компоненти, Hugo shortcodes, AsciiDoc-вкраплення, RST-директиви. Конвенція маркера у `<summary>`: перше слово — аудиторія з фіксованого словника (`Engineer:`/`Ops:`/`Security:`/`Manager:`) → детермінований regex для валідатора. Валідатор отримав чотири нові перевірки: жоден `Engineer:`/`Ops:`/`Security:` блок у manager-only проекціях; кожен `<details>` має оточуючі порожні рядки (інакше `<summary>` рендериться як plain HTML); у `docs/` немає заборонених framework-specific конструкцій; inter-doc посилання працюють для marksman LSP (відносні шляхи або wiki-links). Промпт-скелет LLM-проекцій оновлено — генератор видає `<details>`, не `???`, і дотримується списку заборонених синтаксисів. Subset **forward-compatible**: будь-який збирач (MkDocs, VitePress, Antora) підключається пізніше без переписування контенту. Дзеркало `.cursor/rules/n-ci4.mdc` синхронізується наступним прогоном `npx @nitra/cursor`.
11
+
12
+ ## [1.29.5] - 2026-05-29
13
+
14
+ ### Added
15
+
16
+ - **`rules/js-bun-db/js-bun-db.mdc`** (`version` 1.11 → 1.12) і дзеркало `.cursor/rules/n-js-bun-db.mdc` — нова секція **`## sql.array(arr, type) для передачі масивів`**: обов'язкове використання `pgWrite.array(arr, type)` / `pgRead.array(arr, type)` замість прямої підстановки JS-масиву `${arr}` або cast-синтаксису `${arr}::type[]` у Bun SQL template literal. Другий аргумент типу обов'язковий — без нього Bun не може вивести pg-тип. Секція містить таблицю заборонених/дозволених патернів, таблицю відповідності JS↔PostgreSQL типів і повний приклад UNNEST + MERGE. Рядок `| Масив значень у \`unnest(...)\` | \`sql.array(arr, type)\` — обов'язково з типом |` додано до таблиці рішень.
17
+ - **`rules/bun/bun.mdc`** (`version` 2.0 → 2.1) і дзеркало `.cursor/rules/n-bun.mdc` — `@playwright/test` додано до **root-only** винятків у `devDependencies` кореневого `package.json` (поряд із Vitest/Stryker peer/tools для `n-cursor coverage`).
18
+ - **`rules/bun/policy/package_json/package_json.rego`** — `"@playwright/test"` додано до набору `allowed_root_test_deps`; дозволяє споживачам тримати пакет у root `devDependencies` без порушення bun-policy.
19
+ - **`rules/bun/policy/package_json/package_json_test.rego`** — новий тест `test_allow_playwright_test`: перевіряє, що `@playwright/test` у root `devDependencies` не генерує deny.
6
20
 
7
21
  ## [1.29.4] - 2026-05-29
8
22
 
@@ -40,7 +54,6 @@
40
54
 
41
55
  - **`rules/ci4/ci4.mdc`** (`version` 2.0 → 2.1) — додано секцію **«Зв'язок із `.cursor/rules`»**: архітектурна документація у `docs/` не дублює зміст `.cursor/rules/*.mdc` (операційні правила лінту, тестів, CHANGELOG, версіонування), а посилається на потрібне правило через його ім'я у бектиках (наприклад, `див. **`changelog`**`). Дублікати розходяться з оригіналом і ламають automatic-перевірки `npx @nitra/cursor fix`/`check`; правки робляться в одному місці — у самому `.mdc`.
42
56
 
43
-
44
57
  ## [1.28.8] - 2026-05-28
45
58
 
46
59
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.29.4",
3
+ "version": "1.30.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
package/rules/bun/bun.mdc CHANGED
@@ -2,7 +2,7 @@
2
2
  description: Bun як єдиний package manager у монорепо
3
3
  globs: "**/package.json,**/bunfig.toml,**/bun.lock,**/bun.lockb"
4
4
  alwaysApply: false
5
- version: '2.0'
5
+ version: '2.1'
6
6
  ---
7
7
 
8
8
  Проект використовує тільки Bun для керування залежностями та запуску скриптів.
@@ -42,7 +42,7 @@ Lockfile у репозиторії: `bun.lock`.
42
42
  - Якщо залежність потрібна лише одному пакету, додавати її в директорії цього пакета.
43
43
  - У CI та локально запускати скрипти через `bun run`.
44
44
 
45
- В кореневому `package.json` не повинно бути `dependencies`, а в `devDependencies` — тільки модулі `@nitra/*`. **Виняток (root-only)** — Vitest/Stryker peer/tools для `n-cursor coverage`: `vitest`, `@vitest/coverage-v8`, `@stryker-mutator/vitest-runner`. Тримати їх **у корені** доводиться у будь-якому монорепо-споживачі, бо правило `test` enabled завжди (`test/auto.md` = `завжди`), а класти ці пакети у workspace-и не можна: published пакети (`npm-module.mdc`) не мають мати `devDependencies`, а інші workspace-и однаково запускають coverage оркестратор з кореня. Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn.
45
+ В кореневому `package.json` не повинно бути `dependencies`, а в `devDependencies` — тільки модулі `@nitra/*`. **Виняток (root-only)** — Vitest/Stryker peer/tools для `n-cursor coverage`: `vitest`, `@vitest/coverage-v8`, `@stryker-mutator/vitest-runner`; а також `@playwright/test` для e2e-тестів. Тримати їх **у корені** доводиться у будь-якому монорепо-споживачі, бо правило `test` enabled завжди (`test/auto.md` = `завжди`), а класти ці пакети у workspace-и не можна: published пакети (`npm-module.mdc`) не мають мати `devDependencies`, а інші workspace-и однаково запускають coverage оркестратор з кореня. Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn.
46
46
 
47
47
  - Заборонені top-level поля у root `package.json` (з причинами): [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
48
48
 
@@ -68,7 +68,7 @@ deny contains msg if {
68
68
 
69
69
  # ── helpers ────────────────────────────────────────────────────────────────
70
70
 
71
- allowed_root_test_deps := {"vitest", "@vitest/coverage-v8", "@stryker-mutator/vitest-runner"}
71
+ allowed_root_test_deps := {"vitest", "@vitest/coverage-v8", "@stryker-mutator/vitest-runner", "@playwright/test"}
72
72
 
73
73
  allowed_root_dev_dependency(name) if {
74
74
  startswith(name, "@nitra/")
package/rules/ci4/ci4.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- description: Архітектурна документація продукту — Markdown як джерело істини; рекомендований стек arc42 + Diátaxis + ADR (MADR v4, формат описаний у правилі `adr`) + C4 як набір нотацій; гібридна модель manual + autogen-зон, що регенеруються з accepted ADR; MkDocs Material як viewer з collapsible engineer-блоками для змішаної аудиторії
2
+ description: Архітектурна документація продукту — Markdown як джерело істини; рекомендований стек arc42 + Diátaxis + ADR (MADR v4, формат описаний у правилі `adr`) + C4 як набір нотацій; гібридна модель manual + autogen-зон, що регенеруються з accepted ADR; Zed + marksman LSP як viewer без site-generator-а, portable subset (CommonMark + GFM + Mermaid + KaTeX), collapsible engineer-блоки через нативний `<details>`
3
3
  alwaysApply: true
4
- version: '2.1'
4
+ version: '3.0'
5
5
  ---
6
6
 
7
7
  Архітектурна документація проєкту живе у Markdown поряд із кодом. Це не довідник «для людей із порталу архітектора» — це **джерело істини**, з якого LLM-агент і людина читають намір системи перед будь-якою зміною коду. Тому правила нижче — не оформлення, а робочий процес: який стек використовуємо, як зберігаємо рішення, як автоматично перегенеровуємо проекції з ADR і як рендеримо для змішаної аудиторії (менеджери + інженери + ops).
@@ -47,7 +47,7 @@ RAG витягує **фрагменти**, не цілі документи. Т
47
47
 
48
48
  Доповнюємо **під domain**:
49
49
 
50
- - **DDD Context Maps** — коли є >1 bounded context (типово для multi-agent: agents, orchestration, memory, tooling). Одна Mermaid-діаграма в `explanation/architecture.md`, ховається під `??? engineer`.
50
+ - **DDD Context Maps** — коли є >1 bounded context (типово для multi-agent: agents, orchestration, memory, tooling). Одна Mermaid-діаграма в `explanation/architecture.md`, ховається під `<details><summary><strong>Engineer:</strong> Context Map</summary>`.
51
51
  - **EventModeling** — для event-driven систем (агентські pipeline, CQRS, Event Sourcing). Структура: trigger → command → event → read model. Читається менеджером як наратив, інженером — як креслення.
52
52
  - **Business Capability Map** — верхньорівневий manager-overview. Autogen через семантичну класифікацію accepted ADR за тематикою.
53
53
  - **Wardley Map** — стратегічний артефакт для board/інвесторів. Manual, не autogen. Квартальне оновлення. Живе в `explanation/strategy.md`.
@@ -108,7 +108,7 @@ docs/
108
108
 
109
109
  ## Гібрид manual + autogen
110
110
 
111
- Реальні проєкти мають legacy docs, написані до появи ADR. Розв'язок — **зони** через HTML-коментарі (виживають у Markdown, не рендеряться у MkDocs):
111
+ Реальні проєкти мають legacy docs, написані до появи ADR. Розв'язок — **зони** через HTML-коментарі (виживають у Markdown, не рендеряться у жодному MD viewer — ні в Zed built-in preview, ні на GitHub, ні в будь-якому майбутньому site-generator-і):
112
112
 
113
113
  ```markdown
114
114
  # User Service
@@ -191,61 +191,68 @@ User Service відповідає за автентифікацію та про
191
191
  - **Incremental**: LLM отримує поточний doc + новий ADR (або `## Update`) — видає diff. Дешевше, але дрейф накопичується.
192
192
  - **Компроміс**: incremental + періодичний full rebuild (раз на місяць або після N нових ADR).
193
193
 
194
- ## MkDocs Material: collapsible engineer-блоки
194
+ ## Viewer/editor: Zed + marksman LSP
195
195
 
196
- Менеджер читає прозу зверху, інженер клікає `??? engineer` і провалюється глибше. Один документ дві аудиторії без дублювання.
196
+ Окремого site-generator-а **немає** каталог `docs/` сам є інтерфейсом читання. Інженер відкриває `.md`-файл у Zed і одразу бачить рендер через built-in preview (`cmd+shift+m`). Між сторінками рухається через **`marksman`** LSP: `cmd+click` по `[link](file.md)` і `[[wiki-link]]`, автокомпліт заголовків, find-references, refactor-перейменування заголовків з оновленням посилань у всьому `docs/`.
197
197
 
198
- `mkdocs.yml`:
198
+ `~/.config/zed/settings.json`:
199
199
 
200
- ```yaml
201
- theme:
202
- name: material
203
- features:
204
- - content.code.copy
205
- - navigation.tabs
200
+ ```json
201
+ {
202
+ "languages": {
203
+ "Markdown": {
204
+ "language_servers": ["marksman"],
205
+ "soft_wrap": "editor_width"
206
+ }
207
+ }
208
+ }
209
+ ```
206
210
 
207
- markdown_extensions:
208
- - admonition
209
- - pymdownx.details
210
- - pymdownx.superfences
211
- - pymdownx.tabbed: { alternate_style: true }
212
- - attr_list
211
+ **Portable-only синтаксис.** Усе, що пишемо в `docs/`, обмежене **CommonMark + GFM + Mermaid у fenced code (` ```mermaid `) + KaTeX (`$...$`) + нативний HTML5 `<details>`**. Заборонено:
213
212
 
214
- extra_css:
215
- - stylesheets/audience.css
216
- ```
213
+ - pymdownx admonitions (`!!! note`, `??? engineer`, `=== "tab"`)
214
+ - VitePress containers (`::: tip`, `::: details`)
215
+ - MDX/Astro-компоненти (`<MyComponent />`)
216
+ - Hugo shortcodes (`{{< youtube id >}}`)
217
+ - AsciiDoc-вкраплення, RST-директиви
218
+
219
+ Усе вище не рендериться у Zed built-in preview і ламає головний принцип: **«відкрив файл = побачив фінальний вигляд»**. Обмеження **forward-compatible**: portable subset рендериться у GitHub, MkDocs, VitePress, Antora — підключення збирача в майбутньому не потребує переписування контенту.
217
220
 
218
- Синтаксис у документах:
221
+ ## Collapsible-блоки для змішаної аудиторії
222
+
223
+ Менеджер читає прозу зверху, інженер розгортає деталі. Реалізація — нативний HTML5 `<details>` / `<summary>`, що рендериться **скрізь**: Zed preview, GitHub, будь-який майбутній збирач. Жодних розширень парсера не треба.
219
224
 
220
225
  ```markdown
221
226
  User Service автентифікує користувачів і керує профілями.
222
227
  Підтримуємо OIDC-логін та email/password.
223
228
 
224
- ??? engineer "Технічна реалізація автентифікації"
225
- OIDC-сервер на Fastify з PKCE flow [oidc-pkce-flow].
226
- Refresh-токени з rotation, TTL 30 днів, Redis storage.
229
+ <details>
230
+ <summary><strong>Engineer:</strong> Технічна реалізація автентифікації</summary>
231
+
232
+ OIDC-сервер на Fastify з PKCE flow [oidc-pkce-flow].
233
+ Refresh-токени з rotation, TTL 30 днів, Redis storage.
234
+
235
+ </details>
227
236
 
228
237
  Профілі зберігаються в основній БД, кешуються в Redis на 5 хв.
229
238
 
230
- ??? ops "Деталі кешу та інвалідації"
231
- Cache key: `user:profile:{userId}:v2`.
232
- Інвалідація — NATS подія `user.profile.updated` [user-profile-events].
239
+ <details>
240
+ <summary><strong>Ops:</strong> Деталі кешу та інвалідації</summary>
241
+
242
+ Cache key: `user:profile:{userId}:v2`.
243
+ Інвалідація — NATS подія `user.profile.updated` [user-profile-events].
244
+
245
+ </details>
233
246
  ```
234
247
 
235
- `???` = згорнуто за замовчуванням, `???+` = розгорнуто.
248
+ **Конвенція маркера у `<summary>`:** перше слово аудиторія з фіксованого словника (`Engineer:`, `Ops:`, `Security:`, `Manager:`), далі — описова назва. Це дає валідатору детермінований regex для перевірок типу «жоден `Engineer:`-блок не з'являється в manager-only проекціях».
236
249
 
237
- Кастомні audience-styles у `docs/stylesheets/audience.css`:
250
+ **Технічні нюанси `<details>`:**
238
251
 
239
- ```css
240
- .md-typeset details.engineer > summary {
241
- background-color: rgba(84, 110, 122, 0.1);
242
- border-color: #546e7a;
243
- }
244
- .md-typeset details.ops > summary {
245
- background-color: rgba(255, 152, 0, 0.1);
246
- border-color: #ff9800;
247
- }
248
- ```
252
+ - порожній рядок до `<details>` і після `<summary>` обов'язковий — інакше Markdown-парсер не активує блочний рендер усередині
253
+ - за замовчуванням блок згорнутий; `<details open>` розгорнутий
254
+ - візуальна типізація аудиторій робиться **через текст `<summary>`**, не через CSS — у Zed preview кастомні стилі недоступні
255
+ - `<details>` ≠ HTML-обгортка з правила «жодних `<div>`/`<span>`»: це **семантичний** HTML5-елемент, а не презентаційний; його token-cost мізерний
249
256
 
250
257
  ## Промпт для LLM-проекцій
251
258
 
@@ -259,21 +266,35 @@ User Service автентифікує користувачів і керує п
259
266
  як пов'язано з іншими частинами продукту. Без технологій, версій, коду.
260
267
 
261
268
  2. Технічні деталі — тільки всередині блоків:
262
- ??? engineer "Описова назва блоку"
263
- Код, конфіги, версії, посилання на ADR.
269
+ <details>
270
+ <summary><strong>Engineer:</strong> Описова назва блоку</summary>
271
+
272
+ Код, конфіги, версії, посилання на ADR.
273
+
274
+ </details>
264
275
 
265
276
  3. Operational деталі:
266
- ??? ops "Що моніторити та як реагувати"
277
+ <details>
278
+ <summary><strong>Ops:</strong> Що моніторити та як реагувати</summary>
279
+
280
+ Метрики, алерти, runbook-кроки.
281
+
282
+ </details>
267
283
 
268
284
  Правила:
269
- - Якщо прибрати всі ??? блоки, текст лишається зв'язним менеджерським документом.
270
- - Кожен факт з ADR маркуй [<slug>] всередині engineer-блоку slug це ім'я clean ADR
285
+ - Якщо прибрати всі <details>-блоки, текст лишається зв'язним менеджерським документом.
286
+ - Перше слово у <summary> — аудиторія з фіксованого словника
287
+ (Engineer:/Ops:/Security:/Manager:), далі — описова назва
288
+ (наприклад «<strong>Engineer:</strong> Чому Percona, а не MariaDB»).
289
+ - Порожній рядок до і після вмісту <details> обов'язковий.
290
+ - Кожен факт з ADR маркуй [<slug>] всередині <details>-блоку — slug це ім'я clean ADR
271
291
  без розширення (наприклад `[ланцюжок-запуску-abie]`).
272
- - Назви блоків — описові («Чому Percona, а не MariaDB»).
273
292
  - НЕ виходь за межі <!-- AUTOGEN:start --> ... <!-- AUTOGEN:end -->.
274
293
  - ADR без рядка `**Status:** Accepted` ігноруй.
275
294
  - Якщо в ADR є `## Update YYYY-MM-DD` секції — враховуй найсвіжіший стан рішення,
276
295
  старі формулювання вважай застарілими.
296
+ - Заборонені framework-specific synaxes: !!! note, ??? engineer, ::: tip,
297
+ MDX-компоненти, Hugo shortcodes — тільки CommonMark + GFM + Mermaid + <details>.
277
298
  ```
278
299
 
279
300
  ## Типові поломки LLM і захист
@@ -293,7 +314,10 @@ User Service автентифікує користувачів і керує п
293
314
  - Усі `[<slug>]` посилання вказують на існуючі файли `docs/adr/<slug>.md` із рядком `**Status:** Accepted`
294
315
  - Усі `AUTOGEN:start` мають парний `AUTOGEN:end` з тим самим `id`
295
316
  - Hash у `manifest.json` відповідає фактичному контенту зон
296
- - Жоден `??? engineer` блок не з'являється в manager-only проекціях
317
+ - Жоден `<details>`-блок з `<summary>` що починається на `Engineer:` / `Ops:` / `Security:` не з'являється в manager-only проекціях
318
+ - Кожен `<details>` має оточуючі порожні рядки (інакше `<summary>`-вміст рендериться як plain HTML, не collapsible)
319
+ - У `docs/` немає заборонених framework-specific конструкцій: `!!! note`, `??? `, `::: tip`, `::: details`, MDX-теги `<[A-Z][a-zA-Z]*`, Hugo shortcodes `{{< `
320
+ - Усі inter-doc посилання працюють для `marksman` LSP (відносні шляхи `[text](./other.md)` або `[[wiki-link]]`, не absolute URLs усередині `docs/`)
297
321
  - Якщо в зоні згадано компонент — він є в `docs/glossary.md`
298
322
  - ADR без `**Status:** Accepted` не потрапили у проекцію
299
323
 
@@ -317,7 +341,7 @@ ADR (`docs/adr/<slug>.md`) — джерело правди для autogen-про
317
341
 
318
342
  ## Зв'язок із документацією
319
343
 
320
- Архітектурні артефакти — частина **користувацької документації**, а не закритий артефакт для команди. Контекстна діаграма (C4 рівень 1) і контейнерна (рівень 2) живуть там, де читач шукає вступ у проєкт — у `explanation/architecture.md`, не у відокремленій теці «for-architects». MkDocs Material рендерить це з collapsible-блоками, тому менеджер бачить прозу, інженер провалюється в деталі.
344
+ Архітектурні артефакти — частина **користувацької документації**, а не закритий артефакт для команди. Контекстна діаграма (C4 рівень 1) і контейнерна (рівень 2) живуть там, де читач шукає вступ у проєкт — у `explanation/architecture.md`, не у відокремленій теці «for-architects». Нативний `<details>`-блок дає collapsible-вигляд у будь-якому MD-viewer (Zed built-in preview, GitHub web UI, потенційний майбутній site-generator) без залежності від конкретного фреймворку — менеджер бачить прозу, інженер розгортає деталі.
321
345
 
322
346
  ## Зв'язок із `.cursor/rules`
323
347
 
@@ -330,8 +354,11 @@ ADR (`docs/adr/<slug>.md`) — джерело правди для autogen-про
330
354
  - **Claude Code** як runner — slash-команда `/regen-docs` або post-commit hook на зміни `docs/adr/**`
331
355
  - **capture-decisions.sh** + **normalize-decisions.sh** — Stop-hooks створення ADR (керує правило `adr`)
332
356
  - **gray-matter** на Bun — лише для парсингу draft frontmatter; clean ADR парситься regexp по `**Status:**` / `**Date:**`
333
- - **MkDocs + Material** як viewer
334
- - **Mermaid** для C4-діаграм, EventModeling, Context Maps у проекціях
357
+ - **Zed** як viewer/editor — built-in MD preview (`cmd+shift+m`) рендерить кожен файл без site-generator-а; `<details>` для collapsible-блоків
358
+ - **marksman** LSP як шар навігації `cmd+click` по `[text](file.md)`/`[[wiki-link]]`, find-references, refactor-перейменування заголовків
359
+ - **Mermaid** для C4-діаграм, EventModeling, Context Maps — рендериться у Zed preview напряму з ` ```mermaid ` fenced code, без розширень
360
+ - **KaTeX** (опційно) — інлайн-математика `$...$` для метрик/формул; рендериться у Zed preview
361
+ - Site-generator (MkDocs Material / VitePress / Antora) — **не використовується**; portable subset гарантує, що його можна підключити пізніше без переписування контенту
335
362
 
336
363
  ## Query mode як бонус
337
364
 
@@ -2,7 +2,7 @@
2
2
  description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
3
3
  globs: "**/package.json,**/src/conn/**"
4
4
  alwaysApply: false
5
- version: '1.11'
5
+ version: '1.12'
6
6
  ---
7
7
 
8
8
  ## Підтримувані версії баз даних
@@ -276,6 +276,7 @@ const rows = await sql.unsafe(query, values)
276
276
  | Динамічний `WHERE` (полів багато) | whitelist + ручні `$N` + `sql.unsafe(text, vals)` |
277
277
  | Сирий migration / DDL | `sql.unsafe(text)` з `// allow-unsafe: <причина>` |
278
278
  | User input як value | **тільки** Bun parameters / `$N` bind |
279
+ | Масив значень у `unnest(...)` | `sql.array(arr, type)` — обов'язково з типом |
279
280
 
280
281
  Головне правило:
281
282
 
@@ -384,6 +385,73 @@ await sql.begin(async tx => {
384
385
  })
385
386
  ```
386
387
 
388
+ ## `sql.array(arr, type)` для передачі масивів
389
+
390
+ Коли JS-масив передається як параметр у Bun SQL template literal всередині `unnest(...)` або іншого контексту, де PostgreSQL очікує типізований масив (`int4[]`, `uuid[]` тощо), — обов'язково використовувати `sql.array(arr, type)` (або `pgWrite.array` / `pgRead.array` — вони є екземплярами `SQL`). Другий аргумент (тип елементів) — обов'язковий.
391
+
392
+ ### Заборонені патерни
393
+
394
+ ```javascript
395
+ // ❌ пряма підстановка масиву — Bun серіалізує як рядок, не як pg-масив
396
+ ${ids}
397
+
398
+ // ❌ cast-синтаксис без .array() — працює в деяких версіях, але не гарантований
399
+ ${ids}::int8[]
400
+
401
+ // ❌ відсутній тип — Bun не може вивести тип pg, можливий mismatch
402
+ sql.array(ids)
403
+ ```
404
+
405
+ ### Дозволені патерни
406
+
407
+ ```javascript
408
+ // ✅ pgWrite.array з явним типом
409
+ ${pgWrite.array(ids, 'int8')}
410
+ ${pgWrite.array(uuids, 'uuid')}
411
+ ${pgWrite.array(flags, 'bool')}
412
+ ${pgWrite.array(amounts, 'numeric')}
413
+ ${pgWrite.array(names, 'text')}
414
+ ${pgWrite.array(dates, 'date')}
415
+ ${pgWrite.array(timestamps, 'timestamptz')}
416
+
417
+ // ✅ pgRead.array — те саме правило
418
+ ${pgRead.array(ids, 'int4')}
419
+ ```
420
+
421
+ ### Таблиця типів
422
+
423
+ | JS-тип | PostgreSQL тип | Аргумент |
424
+ | ------------- | -------------- | --------------- |
425
+ | number (int) | int4 | `'int4'` |
426
+ | bigint / id | int8 | `'int8'` |
427
+ | UUID string | uuid | `'uuid'` |
428
+ | boolean | bool | `'bool'` |
429
+ | decimal/float | numeric | `'numeric'` |
430
+ | string | text | `'text'` |
431
+ | date string | date | `'date'` |
432
+ | ISO datetime | timestamptz | `'timestamptz'` |
433
+
434
+ ### Повний приклад (UNNEST + MERGE)
435
+
436
+ ```javascript
437
+ await pgWrite`
438
+ MERGE INTO "order".product p
439
+ USING (
440
+ SELECT * FROM unnest(
441
+ ${pgWrite.array(rows.map(r => r.order_id), 'uuid')},
442
+ ${pgWrite.array(rows.map(r => r.product_id), 'int4')},
443
+ ${pgWrite.array(rows.map(r => r.qty), 'numeric')},
444
+ ${pgWrite.array(rows.map(r => r.is_refund), 'bool')}
445
+ ) AS s(order_id, product_id, qty, is_refund)
446
+ ) AS s ON p.order_id = s.order_id AND p.product_id = s.product_id
447
+ WHEN MATCHED THEN
448
+ UPDATE SET qty = s.qty
449
+ WHEN NOT MATCHED THEN
450
+ INSERT (order_id, product_id, qty, is_refund)
451
+ VALUES (s.order_id, s.product_id, s.qty, s.is_refund)
452
+ `
453
+ ```
454
+
387
455
  ## Коментар під час виправлення SQL injection
388
456
 
389
457
  Коли виправляєш місце з потенційним **SQL injection** (наприклад, заміна конкатенації/`.join(',')` на `sql(ids)` або перехід з `sql.unsafe(...)` на tagged template), **додай поруч короткий коментар** з описом причини.