@nitra/cursor 12.8.6 → 12.8.7

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.
Files changed (187) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -1
  3. package/rules/adr/js/hooks.mdc +32 -0
  4. package/rules/adr/js/madr_format.mdc +96 -0
  5. package/rules/adr/js/settings_policy.mdc +34 -0
  6. package/rules/adr/main.mdc +13 -95
  7. package/rules/bun/js/bunfig.mdc +12 -0
  8. package/rules/bun/js/layout.mdc +60 -0
  9. package/rules/bun/js/lint.mdc +9 -0
  10. package/rules/bun/js/package_json.mdc +19 -0
  11. package/rules/bun/main.mdc +9 -61
  12. package/rules/capacitor/js/ios_spm.mdc +69 -0
  13. package/rules/capacitor/js/version.mdc +29 -0
  14. package/rules/capacitor/main.mdc +8 -22
  15. package/rules/changelog/js/agent-workflow.mdc +15 -0
  16. package/rules/changelog/js/changelog-format.mdc +33 -0
  17. package/rules/changelog/js/comparison-models.mdc +40 -0
  18. package/rules/changelog/main.mdc +4 -98
  19. package/rules/ci4/js/marksman_config.mdc +31 -0
  20. package/rules/ci4/js/vscode_extensions.mdc +33 -0
  21. package/rules/ci4/main.mdc +14 -14
  22. package/rules/docker/js/compile.mdc +44 -0
  23. package/rules/docker/js/hadolint.mdc +50 -0
  24. package/rules/docker/js/mirror.mdc +13 -0
  25. package/rules/docker/js/multistage.mdc +13 -0
  26. package/rules/docker/js/native-addon.mdc +43 -0
  27. package/rules/docker/js/nginx-tag.mdc +7 -0
  28. package/rules/docker/js/nginx-user.mdc +37 -0
  29. package/rules/docker/js/non-root.mdc +39 -0
  30. package/rules/docker/main.mdc +15 -196
  31. package/rules/ga/js/lint_toolchain.mdc +15 -0
  32. package/rules/ga/js/required_workflows.mdc +35 -0
  33. package/rules/ga/js/vscode.mdc +17 -0
  34. package/rules/ga/js/workflow_common.mdc +108 -0
  35. package/rules/ga/js/workflows.mdc +32 -0
  36. package/rules/ga/js/zizmor.mdc +7 -0
  37. package/rules/ga/main.mdc +17 -125
  38. package/rules/graphql/js/tooling.mdc +13 -0
  39. package/rules/graphql/js/vscode_extensions.mdc +13 -0
  40. package/rules/graphql/main.mdc +3 -22
  41. package/rules/hasura/js/internal_urls.mdc +27 -0
  42. package/rules/hasura/js/migrations.mdc +13 -0
  43. package/rules/hasura/js/svc_hl.mdc +17 -0
  44. package/rules/hasura/main.mdc +8 -30
  45. package/rules/image-avif/js/avif_generation.mdc +26 -0
  46. package/rules/image-avif/js/package_json_optout.mdc +21 -0
  47. package/rules/image-avif/main.mdc +7 -34
  48. package/rules/image-compress/js/package_json.mdc +7 -0
  49. package/rules/image-compress/js/package_setup.mdc +13 -0
  50. package/rules/image-compress/main.mdc +4 -12
  51. package/rules/js/docs/index.md +3 -3
  52. package/rules/js/js/dep-policy.mdc +17 -0
  53. package/rules/js/js/eslint-config.mdc +28 -0
  54. package/rules/js/js/extensions.mdc +8 -0
  55. package/rules/js/js/file-extensions.mdc +12 -0
  56. package/rules/js/js/for-in.mdc +26 -0
  57. package/rules/js/js/jscpd.mdc +42 -0
  58. package/rules/js/js/knip.mdc +15 -0
  59. package/rules/js/js/lint-js-workflow.mdc +58 -0
  60. package/rules/js/js/oxlintrc.mdc +20 -0
  61. package/rules/js/js/package-json.mdc +31 -0
  62. package/rules/js/js/tests.mdc +9 -0
  63. package/rules/js/js/utils-lib-structure.mdc +15 -0
  64. package/rules/js/main.mdc +21 -214
  65. package/rules/js-bun-db/js/bun-sql-migration.mdc +15 -0
  66. package/rules/js-bun-db/js/connection.mdc +42 -0
  67. package/rules/js-bun-db/js/pg-format-identifiers.mdc +102 -0
  68. package/rules/js-bun-db/js/pg-format-shim.mdc +99 -0
  69. package/rules/js-bun-db/js/pg-leftover.mdc +27 -0
  70. package/rules/js-bun-db/js/pg-listen-notify.mdc +51 -0
  71. package/rules/js-bun-db/js/query-safety.mdc +117 -0
  72. package/rules/js-bun-db/js/sql-array.mdc +88 -0
  73. package/rules/js-bun-db/js/unsafe.mdc +65 -0
  74. package/rules/js-bun-db/main.mdc +15 -605
  75. package/rules/js-bun-redis/js/imports.mdc +47 -0
  76. package/rules/js-bun-redis/js/package_json.mdc +44 -0
  77. package/rules/js-bun-redis/main.mdc +3 -11
  78. package/rules/js-mssql/js/mssql-in-list.mdc +38 -0
  79. package/rules/js-mssql/js/mssql-pool.mdc +56 -0
  80. package/rules/js-mssql/js/mssql-query-template.mdc +33 -0
  81. package/rules/js-mssql/js/mssql-tvp.mdc +75 -0
  82. package/rules/js-mssql/js/mssql-version.mdc +7 -0
  83. package/rules/js-mssql/main.mdc +10 -198
  84. package/rules/js-run/js/check-env.mdc +35 -0
  85. package/rules/js-run/js/conn-aliases.mdc +109 -0
  86. package/rules/js-run/js/jsconfig.mdc +20 -0
  87. package/rules/js-run/js/otel-configmap.mdc +6 -0
  88. package/rules/js-run/js/pino.mdc +6 -0
  89. package/rules/js-run/js/project-structure.mdc +11 -0
  90. package/rules/js-run/js/runtime.mdc +14 -0
  91. package/rules/js-run/js/scope.mdc +11 -0
  92. package/rules/js-run/js/settimeout.mdc +11 -0
  93. package/rules/js-run/js/temporal.mdc +5 -0
  94. package/rules/js-run/main.mdc +16 -218
  95. package/rules/k8s/js/configmap.mdc +41 -0
  96. package/rules/k8s/js/deployment_resources.mdc +49 -0
  97. package/rules/k8s/js/hasura_httproute.mdc +91 -0
  98. package/rules/k8s/js/hpa_apiversion.mdc +27 -0
  99. package/rules/k8s/js/ingress_gateway.mdc +16 -0
  100. package/rules/k8s/js/kustomize_structure.mdc +144 -0
  101. package/rules/k8s/js/lint_k8s.mdc +72 -0
  102. package/rules/k8s/js/multidoc_yaml.mdc +5 -0
  103. package/rules/k8s/js/network_policy.mdc +136 -0
  104. package/rules/k8s/js/schema_modeline.mdc +57 -0
  105. package/rules/k8s/js/service.mdc +44 -0
  106. package/rules/k8s/js/topology_hpa_pdb.mdc +181 -0
  107. package/rules/k8s/main.mdc +30 -843
  108. package/rules/nginx-default-tpl/js/dockerfile.mdc +36 -0
  109. package/rules/nginx-default-tpl/js/http-route.mdc +41 -0
  110. package/rules/nginx-default-tpl/js/ini-keys.mdc +21 -0
  111. package/rules/nginx-default-tpl/js/template-structure.mdc +86 -0
  112. package/rules/nginx-default-tpl/js/vscode.mdc +37 -0
  113. package/rules/nginx-default-tpl/main.mdc +6 -112
  114. package/rules/npm-module/js/docs/index.md +5 -5
  115. package/rules/npm-module/js/docs/rule_meta.md +6 -6
  116. package/rules/npm-module/js/docs/skill_meta.md +8 -8
  117. package/rules/npm-module/js/header_doc_pointer.mdc +18 -0
  118. package/rules/npm-module/js/package_structure.mdc +62 -0
  119. package/rules/npm-module/js/rule_meta.mdc +11 -0
  120. package/rules/npm-module/js/skill_meta.mdc +11 -0
  121. package/rules/npm-module/main.mdc +10 -55
  122. package/rules/php/js/lint_php_yml.mdc +12 -0
  123. package/rules/php/js/tooling.mdc +66 -0
  124. package/rules/php/main.mdc +7 -66
  125. package/rules/python/js/lint_python_yml.mdc +23 -0
  126. package/rules/python/js/pyproject_toml.mdc +32 -0
  127. package/rules/python/js/tooling.mdc +23 -0
  128. package/rules/python/main.mdc +9 -33
  129. package/rules/rego/js/rego-lint.mdc +31 -0
  130. package/rules/rego/js/vscode_extensions.mdc +11 -0
  131. package/rules/rego/js/vscode_settings.mdc +13 -0
  132. package/rules/rego/main.mdc +8 -24
  133. package/rules/rust/js/coverage.mdc +28 -0
  134. package/rules/rust/js/lint.mdc +22 -0
  135. package/rules/rust/js/tauri_composition.mdc +8 -0
  136. package/rules/rust/js/vscode_extensions.mdc +12 -0
  137. package/rules/rust/main.mdc +8 -38
  138. package/rules/security/js/rego_policies.mdc +15 -0
  139. package/rules/security/js/sample_secret.mdc +19 -0
  140. package/rules/security/js/trufflehog.mdc +21 -0
  141. package/rules/security/main.mdc +7 -35
  142. package/rules/style/js/admin-table.mdc +88 -0
  143. package/rules/style/js/colors.mdc +21 -0
  144. package/rules/style/js/gap.mdc +22 -0
  145. package/rules/style/js/quasar-fixes.mdc +32 -0
  146. package/rules/style/js/quasar.mdc +7 -0
  147. package/rules/style/js/tooling.mdc +85 -0
  148. package/rules/style/main.mdc +13 -253
  149. package/rules/tauri/js/cargo_mutants_config.mdc +39 -0
  150. package/rules/tauri/js/tool_surface.mdc +21 -0
  151. package/rules/tauri/js/tooling.mdc +25 -0
  152. package/rules/tauri/main.mdc +8 -78
  153. package/rules/test/js/cargo_mutants_config.mdc +18 -0
  154. package/rules/test/js/docs/index.md +7 -7
  155. package/rules/test/js/location.mdc +52 -0
  156. package/rules/test/js/no-console-store-restore.mdc +11 -0
  157. package/rules/test/js/no-process-chdir.mdc +15 -0
  158. package/rules/test/js/no-relative-fs-path.mdc +22 -0
  159. package/rules/test/js/sandbox-aware-test.mdc +28 -0
  160. package/rules/test/js/stryker_config.mdc +26 -0
  161. package/rules/test/js/vitest-config-pool-forks.mdc +33 -0
  162. package/rules/test/main.mdc +18 -184
  163. package/rules/text/js/ci-lint-text.mdc +15 -0
  164. package/rules/text/js/cspell.mdc +81 -0
  165. package/rules/text/js/dotenv-linter.mdc +16 -0
  166. package/rules/text/js/forbidden-prettier.mdc +13 -0
  167. package/rules/text/js/markdownlint.mdc +25 -0
  168. package/rules/text/js/oxfmt.mdc +35 -0
  169. package/rules/text/js/package-json.mdc +26 -0
  170. package/rules/text/js/shellcheck.mdc +18 -0
  171. package/rules/text/js/v8r.mdc +23 -0
  172. package/rules/text/js/vscode.mdc +86 -0
  173. package/rules/text/main.mdc +20 -237
  174. package/rules/vue/js/composition-api.mdc +82 -0
  175. package/rules/vue/js/nheader-layout.mdc +171 -0
  176. package/rules/vue/js/node-imports.mdc +25 -0
  177. package/rules/vue/js/quasar-ui.mdc +32 -0
  178. package/rules/vue/js/structure.mdc +101 -0
  179. package/rules/vue/js/testing.mdc +32 -0
  180. package/rules/vue/js/tfm-translations.mdc +26 -0
  181. package/rules/vue/js/vite-config.mdc +126 -0
  182. package/rules/vue/js/vite-env.mdc +55 -0
  183. package/rules/vue/js/vue-imports.mdc +25 -0
  184. package/rules/vue/main.mdc +16 -640
  185. package/scripts/docs/index.md +16 -16
  186. package/scripts/lib/docs/index.md +36 -36
  187. package/scripts/utils/docs/index.md +14 -14
@@ -5,621 +5,31 @@ alwaysApply: false
5
5
  version: '1.15'
6
6
  ---
7
7
 
8
- ## Підтримувані версії баз даних
8
+ Правило забезпечує безпечне підключення й виконання запитів через Bun native SQL замість `pg` / `pg-format` / `mysql2`, із захистом від SQL injection, pg-leftover та небезпечних патернів `sql.unsafe`.
9
9
 
10
- PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом, підключаємось як `mysql://`).
10
+ [js-bun-db-bun-sql-migration](./js/bun-sql-migration.mdc)
11
11
 
12
- ## Заміна на Bun native SQL
12
+ [js-bun-db-pg-format-shim](./js/pg-format-shim.mdc)
13
13
 
14
- Якщо в проєкті використовуються бібліотеки `pg`, `pg-format` або `mysql2`, їх потрібно замінити на Bun native SQL: <https://bun.com/docs/runtime/sql>.
14
+ [js-bun-db-pg-listen-notify](./js/pg-listen-notify.mdc)
15
15
 
16
- - Видалити з `dependencies`: `pg-pool`, `pg-native`, `pg-format`, `mysql`, `mysql2`.
17
- - Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
18
- - Замінити на `import { sql, SQL } from 'bun'` — Bun має вбудований клієнт із пулом, prepared statements та tagged templates.
16
+ [js-bun-db-pg-format-identifiers](./js/pg-format-identifiers.mdc)
19
17
 
20
- Канон заборонених `dependencies` (`pg-format`, `mysql2`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json). Сам `pg` із денилисту прибрано — він має одне легітимне виключення (LISTEN/NOTIFY), яке зважує AST-сканер; деталі — `## pg: виключення для LISTEN/NOTIFY`.
18
+ [js-bun-db-connection](./js/connection.mdc)
21
19
 
22
- `pg-format` (unscoped) — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (`sql\`... ${value} ...\``) і не лишає простору для injection — окремий «форматер» **для значень** не потрібен.
20
+ [js-bun-db-query-safety](./js/query-safety.mdc)
23
21
 
24
- Виключення є **лише** для **динамічних identifiers** (назви схем / таблиць / колонок / індексів / ролей / БД) і whitelist-фрагментів типу `ASC`/`DESC`: Bun SQL їх параметризувати не вміє, тож тут дозволено окремий пакет **`@scaleleap/pg-format`** (scoped форк, не unscoped `pg-format`) — деталі й приклади у `## Динамічна SQL-структура: @scaleleap/pg-format для identifiers`.
22
+ [js-bun-db-sql-array](./js/sql-array.mdc)
25
23
 
26
- ## `pg-format`: повне видалення, без шимів
24
+ [js-bun-db-unsafe](./js/unsafe.mdc)
27
25
 
28
- Міграція з `pg-format` — це **зміна стилю запитів**, а не збереження API. У проєкті після переходу на Bun SQL **заборонено** залишати:
26
+ [js-bun-db-pg-leftover](./js/pg-leftover.mdc)
29
27
 
30
- - функцію з іменем `format` (чи `pgFormat`, `sqlFormat`, `pgFmt`), що приймає шаблон з `%L` / `%I` / `%s` і значення;
31
- - допоміжні `quoteLiteral`, `quoteIdent`, `escapeLiteral`, `escapeIdent` як публічні експорти модуля;
32
- - обгортки `pgRead.query(text, params)` / `pgWrite.query(text, params)` / `db.query(text, params)`, які складають SQL-рядок (з або без `format`) і викликають `sql.unsafe(text, params)` — це повертає injection-поверхню, від якої ми йдемо, тільки під «зручним» іменем.
28
+ ## Швидкий gate через conftest
33
29
 
34
- Замість цього всі точки використання потрібно перевести на tagged template `sql\`...\${value}...\``. Кожне`${value}` стає окремим параметром bind, без рядкового екранування.
30
+ Rego-пакет `js_bun_db.package_json` (файл `policy/package_json/package_json.rego`) перевіряє `dependencies` у `package.json` проти deny-списку [`package.json.deny.json`](./policy/package_json/template/package.json.deny.json):
35
31
 
36
- ### Типові ідіоми `pg-format` → Bun SQL
32
+ - `pg-format` → заміни на Bun native SQL — без ручного форматування
33
+ - `mysql2` → заміни на Bun native SQL
37
34
 
38
- | Було (`pg-format`) | Стало (Bun SQL) |
39
- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
40
- | `format('... WHERE id = %L', id)` | `sql\`... WHERE id = ${id}\`` |
41
- | `format('... IN (%L)', ids)` | `sql\`... IN ${sql(ids)}\`` (з guard на пустоту перед запитом) |
42
- | `format('INSERT ... VALUES %L', [row])` (1 рядок) | `sql\`INSERT ... VALUES (${a}, ${b}, ...)\`` |
43
- | `format('INSERT ... VALUES %L', rows)` (N рядків) | `sql\`INSERT INTO t ${sql(rows, 'a', 'b')}\`` або `unnest($1::T[], $2::T[]) AS t(a, b)` |
44
- | `format('MERGE ... USING (VALUES %L) AS d(...)')` | `sql\`MERGE ... USING (SELECT * FROM unnest(${arrA}::A[], ${arrB}::B[]) AS t(a, b)) AS d\`` |
45
- | `format('... %I ...', tableName)` (whitelist) | `@scaleleap/pg-format`: `format('%I', name)` + `sql.unsafe(text, [params])` з маркером |
46
-
47
- Для multi-row `VALUES` у `MERGE` / `INSERT` з конкретними типами — паралельні масиви по колонках і `unnest($1::TYPE[], $2::TYPE[], ...) AS t(col1, col2, ...)`. Кожна колонка передається одним параметром-масивом; типи задаються кастом масиву (`::uuid[]`, `::bigint[]`, `::numeric[]`, `::text[]`, …).
48
-
49
- #### Приклад: MERGE з UNNEST і динамічними колонками
50
-
51
- ```javascript
52
- // ❌ format + pgWrite.unsafe — N×7 окремих значень, план змінюється при кожному batch.length
53
- const valuesSql = batch
54
- .map(row => format('(%L::int, %L::date, %L::jsonb)', row.id, row.date, JSON.stringify(row.data)))
55
- .join(',')
56
- const sql = format(`MERGE INTO t USING (VALUES %s) AS s(id, date, data) ON ...`, valuesSql)
57
- await pgWrite.unsafe(sql)
58
-
59
- // ✅ UNNEST — 3 параметри незалежно від розміру batch; план стабільний і може кешуватись
60
- await pgWrite`
61
- WITH s(id, date, data) AS (
62
- SELECT * FROM unnest(
63
- ${pgWrite.array(col(batch, 'id'), 'int4')},
64
- ${pgWrite.array(col(batch, 'date'), 'date')},
65
- ${pgWrite.array(col(batch, 'data'), 'jsonb')}
66
- )
67
- )
68
- MERGE INTO my_table AS t
69
- USING s ON t.id = s.id
70
- WHEN MATCHED THEN
71
- UPDATE SET date = s.date, data = s.data
72
- WHEN NOT MATCHED THEN
73
- INSERT (id, date, data) VALUES (s.id, s.date, s.data)
74
- `
75
- ```
76
-
77
- Якщо частина колонок у SET/INSERT залежить від параметра (plan/fact, тип тощо) — динамічні імена колонок не можна параметризувати через `${value}`; використовуй умовні Bun SQL фрагменти:
78
-
79
- ```javascript
80
- // ✅ умовні фрагменти для динамічних ідентифікаторів колонок
81
- const colFrag = isPlan ? pgWrite`plan_value` : pgWrite`fact_value`
82
- const hashFrag = isPlan ? pgWrite`hash = s.hash,` : pgWrite``
83
-
84
- await pgWrite`
85
- ...
86
- WHEN MATCHED THEN
87
- UPDATE SET
88
- ${colFrag} = s.value,
89
- ${hashFrag}
90
- updated_by = s.updated_by
91
- WHEN NOT MATCHED THEN
92
- INSERT (id, ${colFrag}, updated_by)
93
- VALUES (s.id, s.value, s.updated_by)
94
- `
95
- ```
96
-
97
- ## JSONB-параметри: без `JSON.stringify`
98
-
99
- Bun SQL серіалізує JS-об'єкти й масиви у JSON автоматично — викликати `JSON.stringify` перед передачею в `::jsonb` / `::jsonb[]` **заборонено**.
100
-
101
- ```javascript
102
- // ❌ зайвий JSON.stringify — подвійна серіалізація або зайвий рядок
103
- await sql`INSERT INTO events (details) VALUES (${JSON.stringify(detailsForEvent)}::jsonb)`
104
-
105
- await sql`SELECT * FROM unnest(${sql.array(batch.map(r => JSON.stringify(r.data)), 'jsonb')})`
106
-
107
- // ✅ об'єкт/масив передається напряму
108
- await sql`INSERT INTO events (details) VALUES (${detailsForEvent}::jsonb)`
109
-
110
- await sql`SELECT * FROM unnest(${sql.array(col(batch, 'data'), 'jsonb')})`
111
- ```
112
-
113
- `UNION ALL`-цикл замість `unnest` підходить для малих динамічних запитів (2–5 рядків), де кожна гілка семантично різна. Для bulk upsert — завжди `unnest`.
114
-
115
- ### Заборонений «drop-in» шим
116
-
117
- ```javascript
118
- // ❌ pg-format-сумісний шим, що ховає `unsafe` під «безпечним» іменем
119
- export function format(fmt, ...args) {
120
- let i = 0
121
- return fmt.replaceAll(/%[LIs]/g, () => quoteLiteral(args[i++]))
122
- }
123
-
124
- // ❌ і його типовий call-site — той самий injection-вектор, що і прямий sql.unsafe із конкатенацією
125
- await sql.unsafe(format('... WHERE id = %L', userId))
126
- ```
127
-
128
- ```javascript
129
- // ❌ pg-сумісна обгортка над Bun SQL — ще один прихований `unsafe`
130
- export const pgWrite = {
131
- query(text, params) {
132
- return sql.unsafe(text, params)
133
- }
134
- }
135
- ```
136
-
137
- ```javascript
138
- // ✅ напряму tagged template — параметризація через wire-protocol bind
139
- await sql`... WHERE id = ${userId}`
140
- ```
141
-
142
- Виняток для шиму `format()` під час поетапної міграції допускається **тільки** в окремому commit'і з TODO-маркером і дедлайном; готовий код у main з таким шимом — `fail` правила (детектори: `pg-format shim`, `query wrapper over unsafe`).
143
-
144
- ## `pg`: виключення для LISTEN/NOTIFY
145
-
146
- Bun SQL **поки не реалізує PostgreSQL LISTEN/NOTIFY** (асинхронні нотифікації через `pg_notify` / `LISTEN <channel>`). Тому якщо проєкт справді користується LISTEN/NOTIFY, npm-пакет `pg` дозволено тримати в `dependencies` **виключно** для LISTEN/NOTIFY-клієнта. Усі інші запити (SELECT/INSERT/UPDATE/DELETE/migration) — далі через Bun SQL.
147
-
148
- Перевірка `pg` зважує цей сигнал автоматично (тому `pg` прибрано з [denylist](./policy/package_json/template/package.json.deny.json) — Rego не бачить JS-коду, тож зважування LISTEN/NOTIFY перенесено в `check-js-bun-db`).
149
-
150
- ### Як перевірка визначає, що LISTEN/NOTIFY у проєкті є
151
-
152
- AST-сканер шукає будь-який із сигналів:
153
-
154
- - `client.query('LISTEN <channel>')` / `client.query('UNLISTEN *')` / `client.query('NOTIFY <channel>, ...')` — string- або template-literal-аргумент, що починається з `LISTEN` / `UNLISTEN` / `NOTIFY` (case-insensitive, leading whitespace допускається). Також покриті `queryArray` / `queryStream`.
155
- - `client.on('notification', handler)` — listener на pg-події `notification`.
156
- - TaggedTemplateExpression `<tag>\`LISTEN ...\`` — на випадок, якщо хтось загорнув LISTEN у власний tagged template.
157
-
158
- Якщо хоч один сигнал є — `dependencies.pg` зважено як виправдане; інакше — `fail` із посиланням на цю секцію.
159
-
160
- ### Правила для файлів з `import 'pg'`
161
-
162
- Кожен файл, який імпортує `'pg'`, повинен **сам** містити один із LISTEN/NOTIFY-сигналів. Сценарій «один файл слухає, інший виконує `SELECT * FROM users`» — теж `fail`: звичайні запити через `pg` треба переписати на Bun SQL, а LISTEN/NOTIFY-логіку лишити в окремому модулі.
163
-
164
- ### Приклад — окремий модуль для LISTEN
165
-
166
- ```javascript
167
- // src/db/pg-listen.ts — єдине місце, де живе import 'pg'
168
- import { Client } from 'pg'
169
-
170
- const listener = new Client({ connectionString: process.env.DATABASE_URL })
171
-
172
- // allow-pg-leftover: pg LISTEN-клієнт не керується Bun SQL пулом
173
- await listener.connect()
174
- await listener.query('LISTEN orders_channel')
175
- listener.on('notification', msg => {
176
- // обробка нотифікації
177
- })
178
- ```
179
-
180
- ```javascript
181
- // src/db/users.ts — звичайні запити, через Bun SQL
182
- import { sql } from 'bun'
183
-
184
- export const getUser = id => sql`SELECT * FROM users WHERE id = ${id}`
185
- ```
186
-
187
- `pg-listen.ts` буде дозволений завдяки `LISTEN orders_channel` і `.on('notification', ...)`; `users.ts` не має імпорту `'pg'`, тож вільно живе з Bun SQL. `client.connect()` у файлі з Bun SQL потребував би маркер `// allow-pg-leftover: ...`; у файлі, де **Bun SQL не імпортовано**, pg-leftover-сканер не спрацьовує (див. `## Прибирати pg-leftover виклики`), але маркер як коментар-причина — корисний для рев'ю.
188
-
189
- ### Що лишається забороненим
190
-
191
- - `import 'pg'` у файлі без LISTEN/NOTIFY — `fail` з повідомленням «перенеси на Bun SQL, лиши LISTEN в окремому модулі».
192
- - `dependencies.pg` без жодного LISTEN/NOTIFY-сигналу у проєкті — `fail` навіть якщо `pg` нібито «потрібен історично».
193
- - `pg-format` (unscoped) — лишається у [denylist](./policy/package_json/template/package.json.deny.json); виключення для LISTEN/NOTIFY стосується **тільки** самого `pg`.
194
- - `pg-pool`, `pg-native`, `mysql`, `mysql2` — виключень немає, видаляти повністю.
195
-
196
- ## Динамічна SQL-структура: `@scaleleap/pg-format` для identifiers
197
-
198
- Bun SQL **не вміє** параметризувати назви схем, таблиць, колонок, індексів, ролей, БД — а `sql\`SELECT * FROM ${table}\`` забіндив би це як значення і зламав би синтаксис. Для **динамічних identifiers** дозволено окремий пакет:
199
-
200
- ```bash
201
- bun add @scaleleap/pg-format
202
- ```
203
-
204
- ⚠️ Це **scoped `@scaleleap/pg-format`**, а не unscoped `pg-format` (той у [deny-списку](./policy/package_json/template/package.json.deny.json)). Беремо форк `@scaleleap` **тільки** заради `%I` / `%s`-можливостей; значення все одно проходять через Bun parameters, **не** через `%L`.
205
-
206
- ### Дозволений патерн
207
-
208
- - **`%I`** — escape SQL identifier (schema / table / column / index / role / database).
209
- - **`%s`** — raw fragment, **тільки** для whitelist-значень (`ASC` / `DESC`, тип JOIN'у тощо).
210
- - Значення — позиційні параметри `$1, $2, …`, які передаються другим аргументом у `sql.unsafe(query, [bindParams])`.
211
- - На рядку виклику `sql.unsafe(...)` обов'язковий маркер `// allow-unsafe: <причина>` (див. `## sql.unsafe(...) за замовчуванням заборонено`).
212
-
213
- ```javascript
214
- import format from '@scaleleap/pg-format'
215
- import { sql } from 'bun'
216
-
217
- const allowedColumns = new Set(['created_at', 'email', 'name'])
218
- if (!allowedColumns.has(sortBy)) throw new Error('Invalid sort column')
219
-
220
- const direction = sortDir === 'asc' ? 'ASC' : 'DESC'
221
-
222
- const query = format(
223
- 'SELECT * FROM %I.%I ORDER BY %I %s LIMIT $1',
224
- schemaName,
225
- tableName,
226
- sortBy,
227
- direction
228
- )
229
- // allow-unsafe: динамічні schema/table/column; значення біндяться через $N
230
- const rows = await sql.unsafe(query, [limit])
231
- ```
232
-
233
- Multi-row `INSERT` через `VALUES %L` теж типовий легітимний кейс, але передавай значення колонок як паралельні масиви через `unnest(...)` Bun SQL — `format('VALUES %L', rows)` лишай тільки коли альтернатива з `unnest` неможлива:
234
-
235
- ```javascript
236
- const query = format(
237
- /* sql */ `
238
- INSERT INTO "order".delivery_status (order_id, status, changed_at)
239
- SELECT v.order_id::uuid, v.status, v.changed_at::timestamptz
240
- FROM (VALUES %L) AS v(order_id, status, changed_at)
241
- `,
242
- values
243
- )
244
- // allow-unsafe: multi-row VALUES для бекфілу; values формуються з валідованого input
245
- await sql.unsafe(query)
246
- ```
247
-
248
- ### Заборонено й після підключення `@scaleleap/pg-format`
249
-
250
- - **`%L` для user input** — це повернення `pg-format`-стилю. Завжди bind через Bun (`sql\`... = ${value}\``) або позиційний параметр `$N` + `sql.unsafe(query, [params])`.
251
- - Збирати весь `WHERE` через `format(...)` з `%L` — користуйся whitelist полів і ручним складанням `$N`-placeholder'ів (приклад нижче).
252
- - Власні функції `format` / `pgFormat` / `sqlFormat` / `pgFmt` з тілом, що містить `%L` / `%I` / `%s`, — `fail` сканера (це шим, а не імпорт з бібліотеки).
253
- - Експортовані `quoteLiteral` / `quoteIdent` / `escapeLiteral` / `escapeIdent` — `fail` сканера (pg-format-специфічні API замість Bun parameters).
254
-
255
- ### Dynamic `WHERE` — без `format(...)`, через whitelist + `$N`
256
-
257
- ```javascript
258
- const conditions = []
259
- const values = []
260
-
261
- if (email) {
262
- values.push(email)
263
- conditions.push(`email = $${values.length}`)
264
- }
265
- if (status) {
266
- values.push(status)
267
- conditions.push(`status = $${values.length}`)
268
- }
269
-
270
- const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''
271
- const query = `SELECT * FROM users ${where}`
272
- // allow-unsafe: динамічний WHERE з whitelist-полів; значення біндяться через $N
273
- const rows = await sql.unsafe(query, values)
274
- ```
275
-
276
- ### Коротка таблиця рішень
277
-
278
- | Сценарій | Що використовувати |
279
- | --------------------------------- | ---------------------------------------------------- |
280
- | `WHERE id = ${...}` | Bun SQL tagged template |
281
- | `INSERT` одного рядка | Bun SQL tagged template |
282
- | `INSERT` масиву (object/colset) | Bun SQL helper `sql(rows, 'a', 'b')` або `unnest` |
283
- | `UPDATE field = ${value}` | Bun SQL tagged template |
284
- | Динамічна назва schema / table | `@scaleleap/pg-format` `%I` + `sql.unsafe(q, [...])` |
285
- | Динамічна назва колонки | `@scaleleap/pg-format` `%I` + bind |
286
- | Динамічний `ORDER BY column` | whitelist + `%I` |
287
- | `ASC` / `DESC`, тип JOIN'у | whitelist + `%s` |
288
- | Динамічний `WHERE` (полів багато) | whitelist + ручні `$N` + `sql.unsafe(text, vals)` |
289
- | Сирий migration / DDL | `sql.unsafe(text)` з `// allow-unsafe: <причина>` |
290
- | User input як value | **тільки** Bun parameters / `$N` bind |
291
- | Масив значень у `unnest(...)` | `sql.array(arr, type)` — обов'язково з типом |
292
-
293
- Головне правило:
294
-
295
- - **SQL values** → Bun SQL parameters (tagged template `${value}` або `$N` + `sql.unsafe(text, values)`).
296
- - **SQL identifiers** → `@scaleleap/pg-format` `%I` (schema, table, column, index, role, database).
297
- - **SQL fragments** (`ASC`/`DESC` тощо) → whitelist + `%s`.
298
-
299
- ## Підключення (singleton + env)
300
-
301
- Дефолтний експорт `sql` з `'bun'` сам читає змінні середовища (`DATABASE_URL`, `POSTGRES_URL`, `MYSQL_URL`, `PGHOST`/`PGUSER`/... та `MYSQL_HOST`/`MYSQL_USER`/...) і керує пулом — окремий `Pool` як у `pg` створювати не треба.
302
-
303
- Для явного конфігу — `new SQL(...)` як **singleton** на рівні модуля, а не на кожен запит. Файл кладеться у `src/conn/db.mjs` і експортує іменовані константи `pgWrite` (основний запис) та `pgRead` (read-only replica), щоб glob `**/src/conn/**` у правилах покривав ці файли:
304
-
305
- ```javascript
306
- // src/conn/db.mjs
307
- import { SQL } from 'bun'
308
-
309
- export const pgWrite = new SQL({
310
- url: process.env.DATABASE_URL,
311
- max: 20,
312
- idleTimeout: 30,
313
- connectionTimeout: 10
314
- })
315
-
316
- export const pgRead = new SQL({
317
- url: process.env.PG_CONN_READ ?? process.env.DATABASE_URL,
318
- max: 10,
319
- idleTimeout: 30,
320
- connectionTimeout: 10
321
- })
322
- ```
323
-
324
- Connection string обирає адаптер автоматично:
325
-
326
- - `postgres://...` / `postgresql://...` → PostgreSQL
327
- - `mysql://...` / `mysql2://...` → MySQL/MariaDB
328
- - `sqlite://...` / `file://...` / `:memory:` → SQLite
329
-
330
- ## Як виконувати запити (безпечно)
331
-
332
- Тільки **tagged template** з `${...}` — Bun сам біндить позиційні параметри й захищає від SQL injection:
333
-
334
- ```javascript
335
- import { sql } from 'bun'
336
-
337
- const userId = 42
338
- const status = 'active'
339
-
340
- const users = await sql`
341
- SELECT * FROM users
342
- WHERE id = ${userId} AND status = ${status}
343
- `
344
- ```
345
-
346
- Об'єктний INSERT/UPDATE та `IN (...)` — через helper `sql(...)`:
347
-
348
- ```javascript
349
- const user = { name: 'Alice', email: 'a@example.com' }
350
-
351
- const [created] = await sql`
352
- INSERT INTO users ${sql(user)}
353
- RETURNING *
354
- `
355
-
356
- await sql`UPDATE users SET ${sql(user, 'name', 'email')} WHERE id = ${created.id}`
357
-
358
- const ids = [1, 2, 3]
359
- await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
360
- ```
361
-
362
- Multi-row INSERT з масиву об'єктів — `sql(rows)` генерує column list і VALUES автоматично:
363
-
364
- ```javascript
365
- // ❌ format + pgWrite.unsafe — ручне склеювання рядків, injection-вектор
366
- const insertWfQry = `insert into approval.workflow (request_id, job_title_id, name, status)
367
- values ${approverJobs.map(job => `('${request.id}', ${job.id}, '${job.short_name}', 'pending')`).join(', ')}`
368
- await pgWrite.unsafe(insertWfQry)
369
-
370
- // ✅ sql(rows) — один параметр-масив, bind через wire-protocol
371
- const wfRows = approverJobs.map(job => ({
372
- request_id: request.id,
373
- job_title_id: job.id,
374
- name: job.short_name,
375
- status: job.id === nextJobId ? 'current' : 'pending'
376
- }))
377
- await sql`INSERT INTO approval.workflow ${sql(wfRows)}`
378
- ```
379
-
380
- Коли потрібен стабільний план для великих batch'ів (N > 20) або строгі типи колонок — використовуй `unnest` (деталі у `#### Приклад: MERGE з UNNEST і динамічними колонками`). Для невеликих INSERT'ів де колонки відомі — `sql(rows)` коротший і зрозуміліший.
381
-
382
- ## `IN (...)`: значення з template literal — тільки через змінну + guard на пустоту
383
-
384
- Якщо список для `IN (...)` підставляється через `${...}` у template literal, його **потрібно**:
385
-
386
- - винести в **окрему змінну** (не підставляти вираз напряму в `${...}`);
387
- - **перевірити на пустоту** перед запитом і **throw** (щоб не виконувати некоректний SQL або запит з неочікуваною семантикою).
388
-
389
- Приклад:
390
-
391
- ```javascript
392
- const ids = inputIds.map(Number).filter(n => Number.isFinite(n))
393
- if (!ids.length) throw new Error('ids is empty')
394
-
395
- await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
396
- ```
397
-
398
- Транзакції — через `sql.begin` (auto-commit/rollback), вкладені — через `tx.savepoint`:
399
-
400
- ```javascript
401
- await sql.begin(async tx => {
402
- await tx`INSERT INTO users ${sql(user)}`
403
- await tx`UPDATE accounts SET balance = balance - ${100} WHERE user_id = ${user.id}`
404
- })
405
- ```
406
-
407
- ## `sql.array(arr, type)` для передачі масивів
408
-
409
- Коли JS-масив передається як параметр у Bun SQL template literal всередині `unnest(...)` або іншого контексту, де PostgreSQL очікує типізований масив (`int4[]`, `uuid[]` тощо), — обов'язково використовувати `sql.array(arr, type)` (або `pgWrite.array` / `pgRead.array` — вони є екземплярами `SQL`). Другий аргумент (тип елементів) — обов'язковий.
410
-
411
- ### Заборонені патерни
412
-
413
- ```javascript
414
- // ❌ пряма підстановка масиву — Bun серіалізує як рядок, не як pg-масив
415
- ${ids}
416
-
417
- // ❌ cast-синтаксис без .array() — працює в деяких версіях, але не гарантований
418
- ${ids}::int8[]
419
-
420
- // ❌ відсутній тип — Bun не може вивести тип pg, можливий mismatch
421
- sql.array(ids)
422
- ```
423
-
424
- ### Дозволені патерни
425
-
426
- ```javascript
427
- // ✅ pgWrite.array з явним типом
428
- ${pgWrite.array(ids, 'int8')}
429
- ${pgWrite.array(uuids, 'uuid')}
430
- ${pgWrite.array(flags, 'bool')}
431
- ${pgWrite.array(amounts, 'numeric')}
432
- ${pgWrite.array(names, 'text')}
433
- ${pgWrite.array(dates, 'date')}
434
- ${pgWrite.array(timestamps, 'timestamptz')}
435
-
436
- // ✅ pgRead.array — те саме правило
437
- ${pgRead.array(ids, 'int4')}
438
- ```
439
-
440
- ### Таблиця типів
441
-
442
- | JS-тип | PostgreSQL тип | Аргумент |
443
- | ------------- | -------------- | --------------- |
444
- | number (int) | int4 | `'int4'` |
445
- | bigint / id | int8 | `'int8'` |
446
- | UUID string | uuid | `'uuid'` |
447
- | boolean | bool | `'bool'` |
448
- | decimal/float | numeric | `'numeric'` |
449
- | string | text | `'text'` |
450
- | date string | date | `'date'` |
451
- | ISO datetime | timestamptz | `'timestamptz'` |
452
-
453
- ### `col(arr, key)` — хелпер для unnest-колонок
454
-
455
- OXC formatter (oxfmt ≥ 0.49) примусово розгортає будь-який `CallExpression`, де перший аргумент є `CallExpression` з callback, у багаторядковий блок — незалежно від `printWidth`. Тому `pgWrite.array(arr.map(r => r.field), 'type')` всередині tagged template literal завжди стає 4-рядковим блоком. `col(arr, 'field')` (перший аргумент — identifier, другий — string literal) цей тригер не зачіпає і лишається однорядковим.
456
-
457
- Канонічне місце хелпера — `src/utils/col.mjs` (або `src/conn/col.mjs` залежно від структури проєкту):
458
-
459
- ```javascript
460
- // src/utils/col.mjs
461
- export const col = (arr, key) => arr.map(r => r[key])
462
- ```
463
-
464
- ```javascript
465
- import { pgWrite } from '#src/conn/db.mjs'
466
- import { col } from '#src/utils/col.mjs'
467
-
468
- // ❌ oxfmt розгортає на 4+ рядки незалежно від printWidth
469
- ${pgWrite.array(rows.map(r => r.id), 'int4')}
470
-
471
- // ✅ col(arr, key) — перший аргумент не є callback; oxfmt лишає однорядковим
472
- ${pgWrite.array(col(rows, 'id'), 'int4')}
473
- ```
474
-
475
- ### Повний приклад (UNNEST + MERGE)
476
-
477
- ```javascript
478
- await pgWrite`
479
- MERGE INTO "order".product p
480
- USING (
481
- SELECT * FROM unnest(
482
- ${pgWrite.array(col(rows, 'order_id'), 'uuid')},
483
- ${pgWrite.array(col(rows, 'product_id'), 'int4')},
484
- ${pgWrite.array(col(rows, 'qty'), 'numeric')},
485
- ${pgWrite.array(col(rows, 'is_refund'), 'bool')}
486
- ) AS s(order_id, product_id, qty, is_refund)
487
- ) AS s ON p.order_id = s.order_id AND p.product_id = s.product_id
488
- WHEN MATCHED THEN
489
- UPDATE SET qty = s.qty
490
- WHEN NOT MATCHED THEN
491
- INSERT (order_id, product_id, qty, is_refund)
492
- VALUES (s.order_id, s.product_id, s.qty, s.is_refund)
493
- `
494
- ```
495
-
496
- ## Коментар під час виправлення SQL injection
497
-
498
- Коли виправляєш місце з потенційним **SQL injection** (наприклад, заміна конкатенації/`.join(',')` на `sql(ids)` або перехід з `sql.unsafe(...)` на tagged template), **додай поруч короткий коментар** з описом причини.
499
-
500
- Вимоги до коментаря:
501
-
502
- - пояснити **що саме було небезпечно** (конкатенація, підмішування user input, динамічний `IN (...)`, тощо);
503
- - пояснити **чому новий варіант безпечний** (параметризація через tagged template / `sql(...)`);
504
- - без “романів”: 1–2 рядки, достатньо для ревʼю.
505
-
506
- Приклад:
507
-
508
- ```javascript
509
- // SQLi fix: не конкатенуємо значення в `IN (...)`; Bun parameterize через `sql(ids)`.
510
- await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
511
- ```
512
-
513
- ## `sql.unsafe(...)` за замовчуванням заборонено
514
-
515
- Будь-який виклик `sql.unsafe(...)` (так само `tx.unsafe(...)` всередині `sql.begin`) **заборонено**, окрім випадків, коли **обидві** умови виконані:
516
-
517
- 1. значення підставляється з **коду** — константа, конфіг, whitelist; **не з user input**;
518
- 2. треба підставити те, що **не можна параметризувати** через tagged template:
519
- - назву **таблиці**,
520
- - назву **колонки**,
521
- - **dynamic SQL / DDL** (`CREATE`, `ALTER`, `DROP`, multi-statement migration, серверні `SET`/`SHOW` і подібне).
522
-
523
- В усіх інших випадках — переробити на звичайний tagged template `sql\`...\${value}...\``: значення біндяться як параметри й injection не лишається.
524
-
525
- Кожен легітимний `sql.unsafe(...)` має супроводжуватись **маркером-коментарем** з причиною — на тому ж рядку (trailing) або на рядку безпосередньо перед викликом. Маркер — opt-in для перевірки `js-bun-db` і слід для ревʼюера:
526
-
527
- ```javascript
528
- import format from '@scaleleap/pg-format'
529
-
530
- const query = format('CREATE TABLE %I (id int)', tableName)
531
- // allow-unsafe: DDL — назву таблиці параметризувати не можна; ідентифікатор екранує pg-format
532
- await sql.unsafe(query)
533
-
534
- await sql.unsafe('SELECT pg_advisory_lock($1)', [lockId]) // allow-unsafe: pg_advisory_lock — окремий шлях, без tagged template
535
- ```
536
-
537
- Формат маркера: `allow-unsafe: <непорожня причина>` у line- або block-коментарі. Без причини (`// allow-unsafe:`) і без маркера взагалі — **fail** перевірки.
538
-
539
- ### `sql.unsafe` з template-літералом і `${...}`-інтерполяцією — заборонено навіть з маркером
540
-
541
- `sql.unsafe(\`...\${x}...\`)` — окремий **hard fail**, який не знімається маркером `// allow-unsafe`. Шаблонна підстановка`${x}` у `sql.unsafe`-рядок:
542
-
543
- - **не екранує** identifier'ів (reserved words, спецсимволи, пробіли в імені);
544
- - **не біндить** значень (вони потрапляють у запит сирим текстом, як injection-вектор);
545
- - виглядає «безпечно» через знайому tagged-template-форму, але не має жодних гарантій Bun SQL.
546
-
547
- Канон — побудувати `text` окремо, потім передати в `sql.unsafe(text, [params])`:
548
-
549
- - для **identifiers** — `@scaleleap/pg-format` `format('%I', name)` (екранує спецсимволи, reserved words);
550
- - для **values** — позиційні `$1`, `$2`, … як placeholder'и в тексті + масив значень другим аргументом;
551
- - для **fragments** з whitelist (`ASC`/`DESC`) — `format('%s', whitelistedValue)`.
552
-
553
- ```javascript
554
- // ❌ template-літерал з ${...} — fail навіть з allow-unsafe
555
- // allow-unsafe: DDL
556
- await sql.unsafe(`CREATE TABLE ${tableName} (id int)`)
557
-
558
- // ✅ format('%I', ...) екранує identifier, sql.unsafe приймає готовий text
559
- import format from '@scaleleap/pg-format'
560
- const query = format('CREATE TABLE %I (id int)', tableName)
561
- // allow-unsafe: DDL — назву таблиці параметризувати не можна
562
- await sql.unsafe(query)
563
- ```
564
-
565
- Статичні `sql.unsafe(\`SELECT 1\`)` (без `${...}`) і`sql.unsafe(text, [params])` зі змінною `text`, зібраною заздалегідь, — допустимі (за наявності`// allow-unsafe`-маркера).
566
-
567
- ❌ Заборонені кейси (треба переробити на tagged template):
568
-
569
- ```javascript
570
- // ❌ дані від користувача — параметризуй через tagged template
571
- await sql.unsafe(`SELECT * FROM users WHERE id = ${userId}`)
572
-
573
- // ❌ навіть у tagged template — динамічний список через .join(',')
574
- await sql`SELECT * FROM users WHERE id IN (${ids.join(',')})`
575
- ```
576
-
577
- Для динамічних списків — `sql([...])` або `sql(rows, 'colA', 'colB')`, **не** `.join(',')`.
578
-
579
- ## Прибирати pg-leftover виклики (`.connect()`, `.end()`)
580
-
581
- У файлах з Bun SQL (`import { sql, SQL } from 'bun'`) залишки від `pg` — `pool.connect()`, `client.end()`, `pool.end()` — мають бути видалені. Bun SQL пулом керує сам: на першому запиті підключається, idle/lifetime закриває за конфігом — окремий життєвий цикл вручну не потрібен.
582
-
583
- ```javascript
584
- // ❌ pg-leftover: ручний lifecycle, який Bun SQL робить за тебе
585
- const client = await pool.connect()
586
- try {
587
- await client.query('...')
588
- } finally {
589
- await client.end()
590
- }
591
-
592
- // ✅ Bun SQL — без явних .connect()/.end()
593
- await sql`...`
594
- ```
595
-
596
- Якщо виклик дійсно потрібен (наприклад, `sql.end()` у graceful shutdown або `.connect()` на сторонньому об'єкті, що випадково ділить імʼя методу), додай маркер `// allow-pg-leftover: <причина>` на тому ж рядку (trailing) або на рядку безпосередньо перед викликом:
597
-
598
- ```javascript
599
- // allow-pg-leftover: graceful shutdown — закриваємо пул перед exit
600
- await sql.end()
601
-
602
- ws.connect(url) // allow-pg-leftover: WebSocket, не pg
603
- ```
604
-
605
- Формат маркера: `allow-pg-leftover: <непорожня причина>` у line- або block-коментарі. Без маркера й без причини — **fail** перевірки.
606
-
607
- ## Що НЕ робити
608
-
609
- ### Не створювати підключення на кожен запит
610
-
611
- ```javascript
612
- // ❌ нове підключення/інстанс на кожен виклик
613
- function getUser(id) {
614
- const pgWrite = new SQL(process.env.DATABASE_URL)
615
- return pgWrite`SELECT * FROM users WHERE id = ${id}`
616
- }
617
- ```
618
-
619
- `new SQL(...)` має створюватись **один раз** на рівні модуля. Bun сам тримає пул (`max`, `idleTimeout`, `maxLifetime`) — окремих `Pool`/`Client` як у `pg` не потрібно.
620
-
621
- ### Не лишати `pg` / `pg-format` / `mysql2` поряд із Bun SQL
622
-
623
- Якщо в коді з'явився `import { sql } from 'bun'`, то `pg`, `pg-format` та `mysql2` мають бути прибрані і з `dependencies`, і з імпортів — щоб не лишалось двох паралельних шляхів до БД та ручного форматування поряд із параметризованими template literal.
624
-
625
- Те саме стосується **локальних шимів**: будь-який модуль, що експортує `format`, `pgRead`, `pgWrite`, `query(text, params)`, `quoteLiteral`, `quoteIdent` як обгортку над `sql.unsafe(...)`, потрібно переписати — всі call-site на tagged template, сам шим видалити (див. `## pg-format: повне видалення, без шимів`).
35
+ AST-скан коду (`new SQL(...)` у функціях, `unsafe()` без маркера, pg-leftover, динамічні `IN (…)` через `.join(',')`) лишається у JS-перевірці (`js/safety.mjs`). Виключення `pg` для LISTEN/NOTIFY зважується у JS (Rego не бачить JS-коду).