@nitra/cursor 12.8.6 → 12.8.8

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 (263) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/rules/abie/main.mdc +9 -5
  4. package/rules/abie/policy/base_deployment_preem/base_deployment_preem.mdc +22 -0
  5. package/rules/abie/policy/clean_merged_ignore_branches/clean_merged_ignore_branches.mdc +19 -0
  6. package/rules/abie/policy/health_check_policy/health_check_policy.mdc +17 -0
  7. package/rules/abie/policy/http_route_base/http_route_base.mdc +9 -0
  8. package/rules/abie/policy/package_json_shared/package_json_shared.mdc +17 -0
  9. package/rules/adr/js/hooks.mdc +32 -0
  10. package/rules/adr/js/madr_format.mdc +96 -0
  11. package/rules/adr/js/settings_policy.mdc +34 -0
  12. package/rules/adr/main.mdc +17 -95
  13. package/rules/adr/policy/settings_json/settings_json.mdc +7 -0
  14. package/rules/adr/policy/settings_local_json/settings_local_json.mdc +7 -0
  15. package/rules/bun/js/bunfig.mdc +12 -0
  16. package/rules/bun/js/layout.mdc +60 -0
  17. package/rules/bun/js/lint.mdc +9 -0
  18. package/rules/bun/js/package_json.mdc +19 -0
  19. package/rules/bun/main.mdc +7 -60
  20. package/rules/bun/policy/bunfig/bunfig.mdc +12 -0
  21. package/rules/bun/policy/package_json/package_json.mdc +14 -0
  22. package/rules/capacitor/js/ios_spm.mdc +69 -0
  23. package/rules/capacitor/js/version.mdc +29 -0
  24. package/rules/capacitor/main.mdc +6 -22
  25. package/rules/capacitor/policy/package_json/package_json.mdc +9 -0
  26. package/rules/changelog/js/agent-workflow.mdc +15 -0
  27. package/rules/changelog/js/changelog-format.mdc +33 -0
  28. package/rules/changelog/js/comparison-models.mdc +40 -0
  29. package/rules/changelog/main.mdc +4 -98
  30. package/rules/ci4/js/marksman_config.mdc +31 -0
  31. package/rules/ci4/js/vscode_extensions.mdc +33 -0
  32. package/rules/ci4/main.mdc +16 -14
  33. package/rules/ci4/policy/vscode_extensions/vscode_extensions.mdc +9 -0
  34. package/rules/docker/js/compile.mdc +44 -0
  35. package/rules/docker/js/hadolint.mdc +50 -0
  36. package/rules/docker/js/mirror.mdc +13 -0
  37. package/rules/docker/js/multistage.mdc +13 -0
  38. package/rules/docker/js/native-addon.mdc +43 -0
  39. package/rules/docker/js/nginx-tag.mdc +7 -0
  40. package/rules/docker/js/nginx-user.mdc +37 -0
  41. package/rules/docker/js/non-root.mdc +39 -0
  42. package/rules/docker/main.mdc +13 -196
  43. package/rules/docker/policy/lint_docker_yml/lint_docker_yml.mdc +14 -0
  44. package/rules/efes/main.mdc +1 -1
  45. package/rules/efes/policy/package_json_shared/package_json_shared.mdc +30 -0
  46. package/rules/ga/js/lint_toolchain.mdc +15 -0
  47. package/rules/ga/js/required_workflows.mdc +35 -0
  48. package/rules/ga/js/vscode.mdc +17 -0
  49. package/rules/ga/js/workflow_common.mdc +108 -0
  50. package/rules/ga/js/workflows.mdc +32 -0
  51. package/rules/ga/js/zizmor.mdc +7 -0
  52. package/rules/ga/main.mdc +16 -119
  53. package/rules/ga/policy/clean_ga_workflows/clean_ga_workflows.mdc +18 -0
  54. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.mdc +22 -0
  55. package/rules/ga/policy/git_ai/git_ai.mdc +19 -0
  56. package/rules/ga/policy/lint_ga/lint_ga.mdc +21 -0
  57. package/rules/ga/policy/vscode_extensions/vscode_extensions.mdc +9 -0
  58. package/rules/ga/policy/vscode_settings/vscode_settings.mdc +9 -0
  59. package/rules/ga/policy/workflow_common/workflow_common.mdc +18 -0
  60. package/rules/ga/policy/zizmor_yml/zizmor_yml.mdc +9 -0
  61. package/rules/graphql/js/tooling.mdc +13 -0
  62. package/rules/graphql/js/vscode_extensions.mdc +13 -0
  63. package/rules/graphql/main.mdc +4 -21
  64. package/rules/graphql/policy/vscode_extensions/vscode_extensions.mdc +9 -0
  65. package/rules/hasura/js/internal_urls.mdc +27 -0
  66. package/rules/hasura/js/migrations.mdc +13 -0
  67. package/rules/hasura/js/svc_hl.mdc +17 -0
  68. package/rules/hasura/main.mdc +6 -30
  69. package/rules/hasura/policy/svc_hl/svc_hl.mdc +15 -0
  70. package/rules/image-avif/js/avif_generation.mdc +26 -0
  71. package/rules/image-avif/js/package_json_optout.mdc +21 -0
  72. package/rules/image-avif/main.mdc +5 -34
  73. package/rules/image-avif/policy/package_json/package_json.mdc +18 -0
  74. package/rules/image-compress/js/package_json.mdc +7 -0
  75. package/rules/image-compress/js/package_setup.mdc +13 -0
  76. package/rules/image-compress/main.mdc +4 -12
  77. package/rules/image-compress/policy/package_json/package_json.mdc +13 -0
  78. package/rules/js/docs/index.md +3 -3
  79. package/rules/js/js/dep-policy.mdc +17 -0
  80. package/rules/js/js/eslint-config.mdc +28 -0
  81. package/rules/js/js/extensions.mdc +8 -0
  82. package/rules/js/js/file-extensions.mdc +12 -0
  83. package/rules/js/js/for-in.mdc +26 -0
  84. package/rules/js/js/jscpd.mdc +42 -0
  85. package/rules/js/js/knip.mdc +15 -0
  86. package/rules/js/js/lint-js-workflow.mdc +58 -0
  87. package/rules/js/js/oxlintrc.mdc +20 -0
  88. package/rules/js/js/package-json.mdc +31 -0
  89. package/rules/js/js/tests.mdc +9 -0
  90. package/rules/js/js/utils-lib-structure.mdc +15 -0
  91. package/rules/js/main.mdc +19 -211
  92. package/rules/js/policy/jscpd/jscpd.mdc +14 -0
  93. package/rules/js/policy/lint_js_yml/lint_js_yml.mdc +14 -0
  94. package/rules/js/policy/package_json/package_json.mdc +15 -0
  95. package/rules/js/policy/vscode_extensions/vscode_extensions.mdc +11 -0
  96. package/rules/js-bun-db/js/bun-sql-migration.mdc +15 -0
  97. package/rules/js-bun-db/js/connection.mdc +42 -0
  98. package/rules/js-bun-db/js/pg-format-identifiers.mdc +102 -0
  99. package/rules/js-bun-db/js/pg-format-shim.mdc +99 -0
  100. package/rules/js-bun-db/js/pg-leftover.mdc +27 -0
  101. package/rules/js-bun-db/js/pg-listen-notify.mdc +51 -0
  102. package/rules/js-bun-db/js/query-safety.mdc +117 -0
  103. package/rules/js-bun-db/js/sql-array.mdc +88 -0
  104. package/rules/js-bun-db/js/unsafe.mdc +65 -0
  105. package/rules/js-bun-db/main.mdc +12 -607
  106. package/rules/js-bun-db/policy/package_json/package_json.mdc +17 -0
  107. package/rules/js-bun-redis/js/imports.mdc +47 -0
  108. package/rules/js-bun-redis/js/package_json.mdc +44 -0
  109. package/rules/js-bun-redis/main.mdc +4 -10
  110. package/rules/js-bun-redis/policy/package_json/package_json.mdc +11 -0
  111. package/rules/js-mssql/js/mssql-in-list.mdc +38 -0
  112. package/rules/js-mssql/js/mssql-pool.mdc +56 -0
  113. package/rules/js-mssql/js/mssql-query-template.mdc +33 -0
  114. package/rules/js-mssql/js/mssql-tvp.mdc +75 -0
  115. package/rules/js-mssql/js/mssql-version.mdc +7 -0
  116. package/rules/js-mssql/main.mdc +10 -198
  117. package/rules/js-mssql/policy/package_json/package_json.mdc +9 -0
  118. package/rules/js-run/js/check-env.mdc +35 -0
  119. package/rules/js-run/js/conn-aliases.mdc +109 -0
  120. package/rules/js-run/js/jsconfig.mdc +20 -0
  121. package/rules/js-run/js/otel-configmap.mdc +6 -0
  122. package/rules/js-run/js/pino.mdc +6 -0
  123. package/rules/js-run/js/project-structure.mdc +11 -0
  124. package/rules/js-run/js/runtime.mdc +14 -0
  125. package/rules/js-run/js/scope.mdc +11 -0
  126. package/rules/js-run/js/settimeout.mdc +11 -0
  127. package/rules/js-run/js/temporal.mdc +5 -0
  128. package/rules/js-run/main.mdc +16 -216
  129. package/rules/js-run/policy/configmap/configmap.mdc +31 -0
  130. package/rules/js-run/policy/jsconfig/jsconfig.mdc +25 -0
  131. package/rules/js-run/policy/package_json/package_json.mdc +38 -0
  132. package/rules/k8s/js/configmap.mdc +41 -0
  133. package/rules/k8s/js/deployment_resources.mdc +49 -0
  134. package/rules/k8s/js/hasura_httproute.mdc +91 -0
  135. package/rules/k8s/js/hpa_apiversion.mdc +27 -0
  136. package/rules/k8s/js/ingress_gateway.mdc +16 -0
  137. package/rules/k8s/js/kustomize_structure.mdc +144 -0
  138. package/rules/k8s/js/lint_k8s.mdc +72 -0
  139. package/rules/k8s/js/multidoc_yaml.mdc +5 -0
  140. package/rules/k8s/js/network_policy.mdc +136 -0
  141. package/rules/k8s/js/schema_modeline.mdc +57 -0
  142. package/rules/k8s/js/service.mdc +44 -0
  143. package/rules/k8s/js/topology_hpa_pdb.mdc +181 -0
  144. package/rules/k8s/main.mdc +29 -834
  145. package/rules/k8s/policy/base_kustomization/base_kustomization.mdc +12 -0
  146. package/rules/k8s/policy/base_manifest/base_manifest.mdc +14 -0
  147. package/rules/k8s/policy/gateway/gateway.mdc +17 -0
  148. package/rules/k8s/policy/hasura_configmap/hasura_configmap.mdc +20 -0
  149. package/rules/k8s/policy/hasura_httproute/hasura_httproute.mdc +16 -0
  150. package/rules/k8s/policy/hpa_pdb/hpa_pdb.mdc +23 -0
  151. package/rules/k8s/policy/kustomization/kustomization.mdc +20 -0
  152. package/rules/k8s/policy/manifest/manifest.mdc +17 -0
  153. package/rules/k8s/policy/network_policy/network_policy.mdc +22 -0
  154. package/rules/k8s/policy/svc_hl_yaml/svc_hl_yaml.mdc +13 -0
  155. package/rules/k8s/policy/svc_yaml/svc_yaml.mdc +12 -0
  156. package/rules/nginx-default-tpl/js/dockerfile.mdc +36 -0
  157. package/rules/nginx-default-tpl/js/http-route.mdc +41 -0
  158. package/rules/nginx-default-tpl/js/ini-keys.mdc +21 -0
  159. package/rules/nginx-default-tpl/js/template-structure.mdc +86 -0
  160. package/rules/nginx-default-tpl/js/vscode.mdc +37 -0
  161. package/rules/nginx-default-tpl/main.mdc +8 -110
  162. package/rules/nginx-default-tpl/policy/vscode_extensions/vscode_extensions.mdc +11 -0
  163. package/rules/nginx-default-tpl/policy/vscode_settings/vscode_settings.mdc +15 -0
  164. package/rules/npm-module/js/docs/index.md +5 -5
  165. package/rules/npm-module/js/docs/rule_meta.md +6 -6
  166. package/rules/npm-module/js/docs/skill_meta.md +8 -8
  167. package/rules/npm-module/js/header_doc_pointer.mdc +18 -0
  168. package/rules/npm-module/js/package_structure.mdc +62 -0
  169. package/rules/npm-module/js/rule_meta.mdc +11 -0
  170. package/rules/npm-module/js/skill_meta.mdc +11 -0
  171. package/rules/npm-module/main.mdc +10 -52
  172. package/rules/npm-module/policy/emit_types_config/emit_types_config.mdc +40 -0
  173. package/rules/npm-module/policy/npm_package_json/npm_package_json.mdc +50 -0
  174. package/rules/npm-module/policy/root_package_json/root_package_json.mdc +37 -0
  175. package/rules/php/js/lint_php_yml.mdc +12 -0
  176. package/rules/php/js/tooling.mdc +66 -0
  177. package/rules/php/main.mdc +5 -66
  178. package/rules/php/policy/lint_php_yml/lint_php_yml.mdc +21 -0
  179. package/rules/python/js/lint_python_yml.mdc +23 -0
  180. package/rules/python/js/pyproject_toml.mdc +32 -0
  181. package/rules/python/js/tooling.mdc +23 -0
  182. package/rules/python/main.mdc +7 -32
  183. package/rules/python/policy/lint_python_yml/lint_python_yml.mdc +12 -0
  184. package/rules/python/policy/pyproject_toml/pyproject_toml.mdc +13 -0
  185. package/rules/rego/js/rego-lint.mdc +31 -0
  186. package/rules/rego/js/vscode_extensions.mdc +11 -0
  187. package/rules/rego/js/vscode_settings.mdc +13 -0
  188. package/rules/rego/main.mdc +10 -22
  189. package/rules/rego/policy/vscode_extensions/vscode_extensions.mdc +11 -0
  190. package/rules/rego/policy/vscode_settings/vscode_settings.mdc +19 -0
  191. package/rules/rust/js/coverage.mdc +28 -0
  192. package/rules/rust/js/lint.mdc +22 -0
  193. package/rules/rust/js/tauri_composition.mdc +8 -0
  194. package/rules/rust/js/vscode_extensions.mdc +12 -0
  195. package/rules/rust/main.mdc +8 -38
  196. package/rules/rust/policy/lint_rust_yml/lint_rust_yml.mdc +12 -0
  197. package/rules/rust/policy/vscode_extensions/vscode_extensions.mdc +9 -0
  198. package/rules/security/js/rego_policies.mdc +15 -0
  199. package/rules/security/js/sample_secret.mdc +19 -0
  200. package/rules/security/js/trufflehog.mdc +21 -0
  201. package/rules/security/main.mdc +7 -34
  202. package/rules/security/policy/lint_security_yml/lint_security_yml.mdc +7 -0
  203. package/rules/security/policy/package_json/package_json.mdc +7 -0
  204. package/rules/style/js/admin-table.mdc +88 -0
  205. package/rules/style/js/colors.mdc +21 -0
  206. package/rules/style/js/gap.mdc +22 -0
  207. package/rules/style/js/quasar-fixes.mdc +32 -0
  208. package/rules/style/js/quasar.mdc +7 -0
  209. package/rules/style/js/tooling.mdc +85 -0
  210. package/rules/style/main.mdc +12 -251
  211. package/rules/style/policy/lint_style_yml/lint_style_yml.mdc +13 -0
  212. package/rules/style/policy/package_json/package_json.mdc +18 -0
  213. package/rules/style/policy/vscode_extensions/vscode_extensions.mdc +13 -0
  214. package/rules/style/policy/vscode_settings/vscode_settings.mdc +19 -0
  215. package/rules/tauri/js/cargo_mutants_config.mdc +39 -0
  216. package/rules/tauri/js/tool_surface.mdc +21 -0
  217. package/rules/tauri/js/tooling.mdc +25 -0
  218. package/rules/tauri/main.mdc +6 -78
  219. package/rules/tauri/policy/vscode_extensions/vscode_extensions.mdc +21 -0
  220. package/rules/test/js/cargo_mutants_config.mdc +18 -0
  221. package/rules/test/js/docs/index.md +7 -7
  222. package/rules/test/js/location.mdc +52 -0
  223. package/rules/test/js/no-console-store-restore.mdc +11 -0
  224. package/rules/test/js/no-process-chdir.mdc +15 -0
  225. package/rules/test/js/no-relative-fs-path.mdc +22 -0
  226. package/rules/test/js/sandbox-aware-test.mdc +28 -0
  227. package/rules/test/js/stryker_config.mdc +26 -0
  228. package/rules/test/js/vitest-config-pool-forks.mdc +33 -0
  229. package/rules/test/main.mdc +16 -184
  230. package/rules/test/policy/package_json/package_json.mdc +16 -0
  231. package/rules/text/js/ci-lint-text.mdc +15 -0
  232. package/rules/text/js/cspell.mdc +81 -0
  233. package/rules/text/js/dotenv-linter.mdc +16 -0
  234. package/rules/text/js/forbidden-prettier.mdc +13 -0
  235. package/rules/text/js/markdownlint.mdc +25 -0
  236. package/rules/text/js/oxfmt.mdc +35 -0
  237. package/rules/text/js/package-json.mdc +26 -0
  238. package/rules/text/js/shellcheck.mdc +18 -0
  239. package/rules/text/js/v8r.mdc +23 -0
  240. package/rules/text/js/vscode.mdc +86 -0
  241. package/rules/text/main.mdc +20 -231
  242. package/rules/text/policy/cspell/cspell.mdc +34 -0
  243. package/rules/text/policy/lint_text/lint_text.mdc +19 -0
  244. package/rules/text/policy/markdownlint/markdownlint.mdc +38 -0
  245. package/rules/text/policy/oxfmtrc/oxfmtrc.mdc +11 -0
  246. package/rules/text/policy/package_json/package_json.mdc +33 -0
  247. package/rules/text/policy/vscode_extensions/vscode_extensions.mdc +13 -0
  248. package/rules/text/policy/vscode_settings/vscode_settings.mdc +13 -0
  249. package/rules/vue/js/composition-api.mdc +82 -0
  250. package/rules/vue/js/nheader-layout.mdc +171 -0
  251. package/rules/vue/js/node-imports.mdc +25 -0
  252. package/rules/vue/js/quasar-ui.mdc +32 -0
  253. package/rules/vue/js/structure.mdc +101 -0
  254. package/rules/vue/js/testing.mdc +32 -0
  255. package/rules/vue/js/tfm-translations.mdc +26 -0
  256. package/rules/vue/js/vite-config.mdc +126 -0
  257. package/rules/vue/js/vite-env.mdc +55 -0
  258. package/rules/vue/js/vue-imports.mdc +25 -0
  259. package/rules/vue/main.mdc +15 -641
  260. package/rules/vue/policy/package_json/package_json.mdc +30 -0
  261. package/scripts/docs/index.md +16 -16
  262. package/scripts/lib/docs/index.md +36 -36
  263. package/scripts/utils/docs/index.md +14 -14
@@ -0,0 +1,51 @@
1
+ ## `pg`: виключення для LISTEN/NOTIFY
2
+
3
+ Bun SQL **поки не реалізує PostgreSQL LISTEN/NOTIFY** (асинхронні нотифікації через `pg_notify` / `LISTEN <channel>`). Тому якщо проєкт справді користується LISTEN/NOTIFY, npm-пакет `pg` дозволено тримати в `dependencies` **виключно** для LISTEN/NOTIFY-клієнта. Усі інші запити (SELECT/INSERT/UPDATE/DELETE/migration) — далі через Bun SQL.
4
+
5
+ Перевірка `pg` зважує цей сигнал автоматично (тому `pg` прибрано з [denylist](./policy/package_json/template/package.json.deny.json) — Rego не бачить JS-коду, тож зважування LISTEN/NOTIFY перенесено в `check-js-bun-db`).
6
+
7
+ ### Як перевірка визначає, що LISTEN/NOTIFY у проєкті є
8
+
9
+ AST-сканер шукає будь-який із сигналів:
10
+
11
+ - `client.query('LISTEN <channel>')` / `client.query('UNLISTEN *')` / `client.query('NOTIFY <channel>, ...')` — string- або template-literal-аргумент, що починається з `LISTEN` / `UNLISTEN` / `NOTIFY` (case-insensitive, leading whitespace допускається). Також покриті `queryArray` / `queryStream`.
12
+ - `client.on('notification', handler)` — listener на pg-події `notification`.
13
+ - TaggedTemplateExpression `<tag>\`LISTEN ...\`` — на випадок, якщо хтось загорнув LISTEN у власний tagged template.
14
+
15
+ Якщо хоч один сигнал є — `dependencies.pg` зважено як виправдане; інакше — `fail` із посиланням на цю секцію.
16
+
17
+ ### Правила для файлів з `import 'pg'`
18
+
19
+ Кожен файл, який імпортує `'pg'`, повинен **сам** містити один із LISTEN/NOTIFY-сигналів. Сценарій «один файл слухає, інший виконує `SELECT * FROM users`» — теж `fail`: звичайні запити через `pg` треба переписати на Bun SQL, а LISTEN/NOTIFY-логіку лишити в окремому модулі.
20
+
21
+ ### Приклад — окремий модуль для LISTEN
22
+
23
+ ```javascript
24
+ // src/db/pg-listen.ts — єдине місце, де живе import 'pg'
25
+ import { Client } from 'pg'
26
+
27
+ const listener = new Client({ connectionString: process.env.DATABASE_URL })
28
+
29
+ // allow-pg-leftover: pg LISTEN-клієнт не керується Bun SQL пулом
30
+ await listener.connect()
31
+ await listener.query('LISTEN orders_channel')
32
+ listener.on('notification', msg => {
33
+ // обробка нотифікації
34
+ })
35
+ ```
36
+
37
+ ```javascript
38
+ // src/db/users.ts — звичайні запити, через Bun SQL
39
+ import { sql } from 'bun'
40
+
41
+ export const getUser = id => sql`SELECT * FROM users WHERE id = ${id}`
42
+ ```
43
+
44
+ `pg-listen.ts` буде дозволений завдяки `LISTEN orders_channel` і `.on('notification', ...)`; `users.ts` не має імпорту `'pg'`, тож вільно живе з Bun SQL. `client.connect()` у файлі з Bun SQL потребував би маркер `// allow-pg-leftover: ...`; у файлі, де **Bun SQL не імпортовано**, pg-leftover-сканер не спрацьовує, але маркер як коментар-причина — корисний для рев'ю.
45
+
46
+ ### Що лишається забороненим
47
+
48
+ - `import 'pg'` у файлі без LISTEN/NOTIFY — `fail` з повідомленням «перенеси на Bun SQL, лиши LISTEN в окремому модулі».
49
+ - `dependencies.pg` без жодного LISTEN/NOTIFY-сигналу у проєкті — `fail` навіть якщо `pg` нібито «потрібен історично».
50
+ - `pg-format` (unscoped) — лишається у [denylist](./policy/package_json/template/package.json.deny.json); виключення для LISTEN/NOTIFY стосується **тільки** самого `pg`.
51
+ - `pg-pool`, `pg-native`, `mysql`, `mysql2` — виключень немає, видаляти повністю.
@@ -0,0 +1,117 @@
1
+ ## Безпечне виконання запитів
2
+
3
+ Тільки **tagged template** з `${...}` — Bun сам біндить позиційні параметри й захищає від SQL injection:
4
+
5
+ ```javascript
6
+ import { sql } from 'bun'
7
+
8
+ const userId = 42
9
+ const status = 'active'
10
+
11
+ const users = await sql`
12
+ SELECT * FROM users
13
+ WHERE id = ${userId} AND status = ${status}
14
+ `
15
+ ```
16
+
17
+ Об'єктний INSERT/UPDATE та `IN (...)` — через helper `sql(...)`:
18
+
19
+ ```javascript
20
+ const user = { name: 'Alice', email: 'a@example.com' }
21
+
22
+ const [created] = await sql`
23
+ INSERT INTO users ${sql(user)}
24
+ RETURNING *
25
+ `
26
+
27
+ await sql`UPDATE users SET ${sql(user, 'name', 'email')} WHERE id = ${created.id}`
28
+
29
+ const ids = [1, 2, 3]
30
+ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
31
+ ```
32
+
33
+ Multi-row INSERT з масиву об'єктів — `sql(rows)` генерує column list і VALUES автоматично:
34
+
35
+ ```javascript
36
+ // ❌ format + pgWrite.unsafe — ручне склеювання рядків, injection-вектор
37
+ const insertWfQry = `insert into approval.workflow (request_id, job_title_id, name, status)
38
+ values ${approverJobs.map(job => `('${request.id}', ${job.id}, '${job.short_name}', 'pending')`).join(', ')}`
39
+ await pgWrite.unsafe(insertWfQry)
40
+
41
+ // ✅ sql(rows) — один параметр-масив, bind через wire-protocol
42
+ const wfRows = approverJobs.map(job => ({
43
+ request_id: request.id,
44
+ job_title_id: job.id,
45
+ name: job.short_name,
46
+ status: job.id === nextJobId ? 'current' : 'pending'
47
+ }))
48
+ await sql`INSERT INTO approval.workflow ${sql(wfRows)}`
49
+ ```
50
+
51
+ Коли потрібен стабільний план для великих batch'ів (N > 20) або строгі типи колонок — використовуй `unnest` (деталі у `js/pg-format-shim.mdc`, приклад MERGE з UNNEST). Для невеликих INSERT'ів де колонки відомі — `sql(rows)` коротший і зрозуміліший.
52
+
53
+ ## `IN (...)`: значення з template literal — тільки через змінну + guard на пустоту
54
+
55
+ Якщо список для `IN (...)` підставляється через `${...}` у template literal, його **потрібно**:
56
+
57
+ - винести в **окрему змінну** (не підставляти вираз напряму в `${...}`);
58
+ - **перевірити на пустоту** перед запитом і **throw** (щоб не виконувати некоректний SQL або запит з неочікуваною семантикою).
59
+
60
+ Приклад:
61
+
62
+ ```javascript
63
+ const ids = inputIds.map(Number).filter(n => Number.isFinite(n))
64
+ if (!ids.length) throw new Error('ids is empty')
65
+
66
+ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
67
+ ```
68
+
69
+ Транзакції — через `sql.begin` (auto-commit/rollback), вкладені — через `tx.savepoint`:
70
+
71
+ ```javascript
72
+ await sql.begin(async tx => {
73
+ await tx`INSERT INTO users ${sql(user)}`
74
+ await tx`UPDATE accounts SET balance = balance - ${100} WHERE user_id = ${user.id}`
75
+ })
76
+ ```
77
+
78
+ ## JSONB-параметри: без `JSON.stringify`
79
+
80
+ Bun SQL серіалізує JS-об'єкти й масиви у JSON автоматично — викликати `JSON.stringify` перед передачею в `::jsonb` / `::jsonb[]` **заборонено**.
81
+
82
+ ```javascript
83
+ // ❌ зайвий JSON.stringify — подвійна серіалізація або зайвий рядок
84
+ await sql`INSERT INTO events (details) VALUES (${JSON.stringify(detailsForEvent)}::jsonb)`
85
+
86
+ await sql`SELECT * FROM unnest(${sql.array(batch.map(r => JSON.stringify(r.data)), 'jsonb')})`
87
+
88
+ // ✅ об'єкт/масив передається напряму
89
+ await sql`INSERT INTO events (details) VALUES (${detailsForEvent}::jsonb)`
90
+
91
+ await sql`SELECT * FROM unnest(${sql.array(col(batch, 'data'), 'jsonb')})`
92
+ ```
93
+
94
+ `UNION ALL`-цикл замість `unnest` підходить для малих динамічних запитів (2–5 рядків), де кожна гілка семантично різна. Для bulk upsert — завжди `unnest`.
95
+
96
+ ## Коментар під час виправлення SQL injection
97
+
98
+ Коли виправляєш місце з потенційним **SQL injection** (наприклад, заміна конкатенації/`.join(',')` на `sql(ids)` або перехід з `sql.unsafe(...)` на tagged template), **додай поруч короткий коментар** з описом причини.
99
+
100
+ Вимоги до коментаря:
101
+
102
+ - пояснити **що саме було небезпечно** (конкатенація, підмішування user input, динамічний `IN (...)`, тощо);
103
+ - пояснити **чому новий варіант безпечний** (параметризація через tagged template / `sql(...)`);
104
+ - без "романів": 1–2 рядки, достатньо для ревʼю.
105
+
106
+ Приклад:
107
+
108
+ ```javascript
109
+ // SQLi fix: не конкатенуємо значення в `IN (...)`; Bun parameterize через `sql(ids)`.
110
+ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
111
+ ```
112
+
113
+ ## Що НЕ робити з бібліотеками
114
+
115
+ Якщо в коді з'явився `import { sql } from 'bun'`, то `pg`, `pg-format` та `mysql2` мають бути прибрані і з `dependencies`, і з імпортів — щоб не лишалось двох паралельних шляхів до БД та ручного форматування поряд із параметризованими template literal.
116
+
117
+ Те саме стосується **локальних шимів**: будь-який модуль, що експортує `format`, `pgRead`, `pgWrite`, `query(text, params)`, `quoteLiteral`, `quoteIdent` як обгортку над `sql.unsafe(...)`, потрібно переписати — всі call-site на tagged template, сам шим видалити (деталі у `js/pg-format-shim.mdc`).
@@ -0,0 +1,88 @@
1
+ ## `sql.array(arr, type)` для передачі масивів
2
+
3
+ Коли JS-масив передається як параметр у Bun SQL template literal всередині `unnest(...)` або іншого контексту, де PostgreSQL очікує типізований масив (`int4[]`, `uuid[]` тощо), — обов'язково використовувати `sql.array(arr, type)` (або `pgWrite.array` / `pgRead.array` — вони є екземплярами `SQL`). Другий аргумент (тип елементів) — обов'язковий.
4
+
5
+ ### Заборонені патерни
6
+
7
+ ```javascript
8
+ // ❌ пряма підстановка масиву — Bun серіалізує як рядок, не як pg-масив
9
+ ${ids}
10
+
11
+ // ❌ cast-синтаксис без .array() — працює в деяких версіях, але не гарантований
12
+ ${ids}::int8[]
13
+
14
+ // ❌ відсутній тип — Bun не може вивести тип pg, можливий mismatch
15
+ sql.array(ids)
16
+ ```
17
+
18
+ ### Дозволені патерни
19
+
20
+ ```javascript
21
+ // ✅ pgWrite.array з явним типом
22
+ ${pgWrite.array(ids, 'int8')}
23
+ ${pgWrite.array(uuids, 'uuid')}
24
+ ${pgWrite.array(flags, 'bool')}
25
+ ${pgWrite.array(amounts, 'numeric')}
26
+ ${pgWrite.array(names, 'text')}
27
+ ${pgWrite.array(dates, 'date')}
28
+ ${pgWrite.array(timestamps, 'timestamptz')}
29
+
30
+ // ✅ pgRead.array — те саме правило
31
+ ${pgRead.array(ids, 'int4')}
32
+ ```
33
+
34
+ ### Таблиця типів
35
+
36
+ | JS-тип | PostgreSQL тип | Аргумент |
37
+ | ------------- | -------------- | --------------- |
38
+ | number (int) | int4 | `'int4'` |
39
+ | bigint / id | int8 | `'int8'` |
40
+ | UUID string | uuid | `'uuid'` |
41
+ | boolean | bool | `'bool'` |
42
+ | decimal/float | numeric | `'numeric'` |
43
+ | string | text | `'text'` |
44
+ | date string | date | `'date'` |
45
+ | ISO datetime | timestamptz | `'timestamptz'` |
46
+
47
+ ### `col(arr, key)` — хелпер для unnest-колонок
48
+
49
+ 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) цей тригер не зачіпає і лишається однорядковим.
50
+
51
+ Канонічне місце хелпера — `src/utils/col.mjs` (або `src/conn/col.mjs` залежно від структури проєкту):
52
+
53
+ ```javascript
54
+ // src/utils/col.mjs
55
+ export const col = (arr, key) => arr.map(r => r[key])
56
+ ```
57
+
58
+ ```javascript
59
+ import { pgWrite } from '#src/conn/db.mjs'
60
+ import { col } from '#src/utils/col.mjs'
61
+
62
+ // ❌ oxfmt розгортає на 4+ рядки незалежно від printWidth
63
+ ${pgWrite.array(rows.map(r => r.id), 'int4')}
64
+
65
+ // ✅ col(arr, key) — перший аргумент не є callback; oxfmt лишає однорядковим
66
+ ${pgWrite.array(col(rows, 'id'), 'int4')}
67
+ ```
68
+
69
+ ### Повний приклад (UNNEST + MERGE)
70
+
71
+ ```javascript
72
+ await pgWrite`
73
+ MERGE INTO "order".product p
74
+ USING (
75
+ SELECT * FROM unnest(
76
+ ${pgWrite.array(col(rows, 'order_id'), 'uuid')},
77
+ ${pgWrite.array(col(rows, 'product_id'), 'int4')},
78
+ ${pgWrite.array(col(rows, 'qty'), 'numeric')},
79
+ ${pgWrite.array(col(rows, 'is_refund'), 'bool')}
80
+ ) AS s(order_id, product_id, qty, is_refund)
81
+ ) AS s ON p.order_id = s.order_id AND p.product_id = s.product_id
82
+ WHEN MATCHED THEN
83
+ UPDATE SET qty = s.qty
84
+ WHEN NOT MATCHED THEN
85
+ INSERT (order_id, product_id, qty, is_refund)
86
+ VALUES (s.order_id, s.product_id, s.qty, s.is_refund)
87
+ `
88
+ ```
@@ -0,0 +1,65 @@
1
+ ## `sql.unsafe(...)` за замовчуванням заборонено
2
+
3
+ Будь-який виклик `sql.unsafe(...)` (так само `tx.unsafe(...)` всередині `sql.begin`) **заборонено**, окрім випадків, коли **обидві** умови виконані:
4
+
5
+ 1. значення підставляється з **коду** — константа, конфіг, whitelist; **не з user input**;
6
+ 2. треба підставити те, що **не можна параметризувати** через tagged template:
7
+ - назву **таблиці**,
8
+ - назву **колонки**,
9
+ - **dynamic SQL / DDL** (`CREATE`, `ALTER`, `DROP`, multi-statement migration, серверні `SET`/`SHOW` і подібне).
10
+
11
+ В усіх інших випадках — переробити на звичайний tagged template `sql\`...\${value}...\``: значення біндяться як параметри й injection не лишається.
12
+
13
+ Кожен легітимний `sql.unsafe(...)` має супроводжуватись **маркером-коментарем** з причиною — на тому ж рядку (trailing) або на рядку безпосередньо перед викликом. Маркер — opt-in для перевірки `js-bun-db` і слід для ревʼюера:
14
+
15
+ ```javascript
16
+ import format from '@scaleleap/pg-format'
17
+
18
+ const query = format('CREATE TABLE %I (id int)', tableName)
19
+ // allow-unsafe: DDL — назву таблиці параметризувати не можна; ідентифікатор екранує pg-format
20
+ await sql.unsafe(query)
21
+
22
+ await sql.unsafe('SELECT pg_advisory_lock($1)', [lockId]) // allow-unsafe: pg_advisory_lock — окремий шлях, без tagged template
23
+ ```
24
+
25
+ Формат маркера: `allow-unsafe: <непорожня причина>` у line- або block-коментарі. Без причини (`// allow-unsafe:`) і без маркера взагалі — **fail** перевірки.
26
+
27
+ ### `sql.unsafe` з template-літералом і `${...}`-інтерполяцією — заборонено навіть з маркером
28
+
29
+ `sql.unsafe(\`...\${x}...\`)` — окремий **hard fail**, який не знімається маркером `// allow-unsafe`. Шаблонна підстановка `${x}` у `sql.unsafe`-рядок:
30
+
31
+ - **не екранує** identifier'ів (reserved words, спецсимволи, пробіли в імені);
32
+ - **не біндить** значень (вони потрапляють у запит сирим текстом, як injection-вектор);
33
+ - виглядає «безпечно» через знайому tagged-template-форму, але не має жодних гарантій Bun SQL.
34
+
35
+ Канон — побудувати `text` окремо, потім передати в `sql.unsafe(text, [params])`:
36
+
37
+ - для **identifiers** — `@scaleleap/pg-format` `format('%I', name)` (екранує спецсимволи, reserved words);
38
+ - для **values** — позиційні `$1`, `$2`, … як placeholder'и в тексті + масив значень другим аргументом;
39
+ - для **fragments** з whitelist (`ASC`/`DESC`) — `format('%s', whitelistedValue)`.
40
+
41
+ ```javascript
42
+ // ❌ template-літерал з ${...} — fail навіть з allow-unsafe
43
+ // allow-unsafe: DDL
44
+ await sql.unsafe(`CREATE TABLE ${tableName} (id int)`)
45
+
46
+ // ✅ format('%I', ...) екранує identifier, sql.unsafe приймає готовий text
47
+ import format from '@scaleleap/pg-format'
48
+ const query = format('CREATE TABLE %I (id int)', tableName)
49
+ // allow-unsafe: DDL — назву таблиці параметризувати не можна
50
+ await sql.unsafe(query)
51
+ ```
52
+
53
+ Статичні `sql.unsafe(\`SELECT 1\`)` (без `${...}`) і `sql.unsafe(text, [params])` зі змінною `text`, зібраною заздалегідь, — допустимі (за наявності `// allow-unsafe`-маркера).
54
+
55
+ Заборонені кейси (треба переробити на tagged template):
56
+
57
+ ```javascript
58
+ // ❌ дані від користувача — параметризуй через tagged template
59
+ await sql.unsafe(`SELECT * FROM users WHERE id = ${userId}`)
60
+
61
+ // ❌ навіть у tagged template — динамічний список через .join(',')
62
+ await sql`SELECT * FROM users WHERE id IN (${ids.join(',')})`
63
+ ```
64
+
65
+ Для динамічних списків — `sql([...])` або `sql(rows, 'colA', 'colB')`, **не** `.join(',')`.