@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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/rules/abie/main.mdc +9 -5
- package/rules/abie/policy/base_deployment_preem/base_deployment_preem.mdc +22 -0
- package/rules/abie/policy/clean_merged_ignore_branches/clean_merged_ignore_branches.mdc +19 -0
- package/rules/abie/policy/health_check_policy/health_check_policy.mdc +17 -0
- package/rules/abie/policy/http_route_base/http_route_base.mdc +9 -0
- package/rules/abie/policy/package_json_shared/package_json_shared.mdc +17 -0
- package/rules/adr/js/hooks.mdc +32 -0
- package/rules/adr/js/madr_format.mdc +96 -0
- package/rules/adr/js/settings_policy.mdc +34 -0
- package/rules/adr/main.mdc +17 -95
- package/rules/adr/policy/settings_json/settings_json.mdc +7 -0
- package/rules/adr/policy/settings_local_json/settings_local_json.mdc +7 -0
- package/rules/bun/js/bunfig.mdc +12 -0
- package/rules/bun/js/layout.mdc +60 -0
- package/rules/bun/js/lint.mdc +9 -0
- package/rules/bun/js/package_json.mdc +19 -0
- package/rules/bun/main.mdc +7 -60
- package/rules/bun/policy/bunfig/bunfig.mdc +12 -0
- package/rules/bun/policy/package_json/package_json.mdc +14 -0
- package/rules/capacitor/js/ios_spm.mdc +69 -0
- package/rules/capacitor/js/version.mdc +29 -0
- package/rules/capacitor/main.mdc +6 -22
- package/rules/capacitor/policy/package_json/package_json.mdc +9 -0
- package/rules/changelog/js/agent-workflow.mdc +15 -0
- package/rules/changelog/js/changelog-format.mdc +33 -0
- package/rules/changelog/js/comparison-models.mdc +40 -0
- package/rules/changelog/main.mdc +4 -98
- package/rules/ci4/js/marksman_config.mdc +31 -0
- package/rules/ci4/js/vscode_extensions.mdc +33 -0
- package/rules/ci4/main.mdc +16 -14
- package/rules/ci4/policy/vscode_extensions/vscode_extensions.mdc +9 -0
- package/rules/docker/js/compile.mdc +44 -0
- package/rules/docker/js/hadolint.mdc +50 -0
- package/rules/docker/js/mirror.mdc +13 -0
- package/rules/docker/js/multistage.mdc +13 -0
- package/rules/docker/js/native-addon.mdc +43 -0
- package/rules/docker/js/nginx-tag.mdc +7 -0
- package/rules/docker/js/nginx-user.mdc +37 -0
- package/rules/docker/js/non-root.mdc +39 -0
- package/rules/docker/main.mdc +13 -196
- package/rules/docker/policy/lint_docker_yml/lint_docker_yml.mdc +14 -0
- package/rules/efes/main.mdc +1 -1
- package/rules/efes/policy/package_json_shared/package_json_shared.mdc +30 -0
- package/rules/ga/js/lint_toolchain.mdc +15 -0
- package/rules/ga/js/required_workflows.mdc +35 -0
- package/rules/ga/js/vscode.mdc +17 -0
- package/rules/ga/js/workflow_common.mdc +108 -0
- package/rules/ga/js/workflows.mdc +32 -0
- package/rules/ga/js/zizmor.mdc +7 -0
- package/rules/ga/main.mdc +16 -119
- package/rules/ga/policy/clean_ga_workflows/clean_ga_workflows.mdc +18 -0
- package/rules/ga/policy/clean_merged_branch/clean_merged_branch.mdc +22 -0
- package/rules/ga/policy/git_ai/git_ai.mdc +19 -0
- package/rules/ga/policy/lint_ga/lint_ga.mdc +21 -0
- package/rules/ga/policy/vscode_extensions/vscode_extensions.mdc +9 -0
- package/rules/ga/policy/vscode_settings/vscode_settings.mdc +9 -0
- package/rules/ga/policy/workflow_common/workflow_common.mdc +18 -0
- package/rules/ga/policy/zizmor_yml/zizmor_yml.mdc +9 -0
- package/rules/graphql/js/tooling.mdc +13 -0
- package/rules/graphql/js/vscode_extensions.mdc +13 -0
- package/rules/graphql/main.mdc +4 -21
- package/rules/graphql/policy/vscode_extensions/vscode_extensions.mdc +9 -0
- package/rules/hasura/js/internal_urls.mdc +27 -0
- package/rules/hasura/js/migrations.mdc +13 -0
- package/rules/hasura/js/svc_hl.mdc +17 -0
- package/rules/hasura/main.mdc +6 -30
- package/rules/hasura/policy/svc_hl/svc_hl.mdc +15 -0
- package/rules/image-avif/js/avif_generation.mdc +26 -0
- package/rules/image-avif/js/package_json_optout.mdc +21 -0
- package/rules/image-avif/main.mdc +5 -34
- package/rules/image-avif/policy/package_json/package_json.mdc +18 -0
- package/rules/image-compress/js/package_json.mdc +7 -0
- package/rules/image-compress/js/package_setup.mdc +13 -0
- package/rules/image-compress/main.mdc +4 -12
- package/rules/image-compress/policy/package_json/package_json.mdc +13 -0
- package/rules/js/docs/index.md +3 -3
- package/rules/js/js/dep-policy.mdc +17 -0
- package/rules/js/js/eslint-config.mdc +28 -0
- package/rules/js/js/extensions.mdc +8 -0
- package/rules/js/js/file-extensions.mdc +12 -0
- package/rules/js/js/for-in.mdc +26 -0
- package/rules/js/js/jscpd.mdc +42 -0
- package/rules/js/js/knip.mdc +15 -0
- package/rules/js/js/lint-js-workflow.mdc +58 -0
- package/rules/js/js/oxlintrc.mdc +20 -0
- package/rules/js/js/package-json.mdc +31 -0
- package/rules/js/js/tests.mdc +9 -0
- package/rules/js/js/utils-lib-structure.mdc +15 -0
- package/rules/js/main.mdc +19 -211
- package/rules/js/policy/jscpd/jscpd.mdc +14 -0
- package/rules/js/policy/lint_js_yml/lint_js_yml.mdc +14 -0
- package/rules/js/policy/package_json/package_json.mdc +15 -0
- package/rules/js/policy/vscode_extensions/vscode_extensions.mdc +11 -0
- package/rules/js-bun-db/js/bun-sql-migration.mdc +15 -0
- package/rules/js-bun-db/js/connection.mdc +42 -0
- package/rules/js-bun-db/js/pg-format-identifiers.mdc +102 -0
- package/rules/js-bun-db/js/pg-format-shim.mdc +99 -0
- package/rules/js-bun-db/js/pg-leftover.mdc +27 -0
- package/rules/js-bun-db/js/pg-listen-notify.mdc +51 -0
- package/rules/js-bun-db/js/query-safety.mdc +117 -0
- package/rules/js-bun-db/js/sql-array.mdc +88 -0
- package/rules/js-bun-db/js/unsafe.mdc +65 -0
- package/rules/js-bun-db/main.mdc +12 -607
- package/rules/js-bun-db/policy/package_json/package_json.mdc +17 -0
- package/rules/js-bun-redis/js/imports.mdc +47 -0
- package/rules/js-bun-redis/js/package_json.mdc +44 -0
- package/rules/js-bun-redis/main.mdc +4 -10
- package/rules/js-bun-redis/policy/package_json/package_json.mdc +11 -0
- package/rules/js-mssql/js/mssql-in-list.mdc +38 -0
- package/rules/js-mssql/js/mssql-pool.mdc +56 -0
- package/rules/js-mssql/js/mssql-query-template.mdc +33 -0
- package/rules/js-mssql/js/mssql-tvp.mdc +75 -0
- package/rules/js-mssql/js/mssql-version.mdc +7 -0
- package/rules/js-mssql/main.mdc +10 -198
- package/rules/js-mssql/policy/package_json/package_json.mdc +9 -0
- package/rules/js-run/js/check-env.mdc +35 -0
- package/rules/js-run/js/conn-aliases.mdc +109 -0
- package/rules/js-run/js/jsconfig.mdc +20 -0
- package/rules/js-run/js/otel-configmap.mdc +6 -0
- package/rules/js-run/js/pino.mdc +6 -0
- package/rules/js-run/js/project-structure.mdc +11 -0
- package/rules/js-run/js/runtime.mdc +14 -0
- package/rules/js-run/js/scope.mdc +11 -0
- package/rules/js-run/js/settimeout.mdc +11 -0
- package/rules/js-run/js/temporal.mdc +5 -0
- package/rules/js-run/main.mdc +16 -216
- package/rules/js-run/policy/configmap/configmap.mdc +31 -0
- package/rules/js-run/policy/jsconfig/jsconfig.mdc +25 -0
- package/rules/js-run/policy/package_json/package_json.mdc +38 -0
- package/rules/k8s/js/configmap.mdc +41 -0
- package/rules/k8s/js/deployment_resources.mdc +49 -0
- package/rules/k8s/js/hasura_httproute.mdc +91 -0
- package/rules/k8s/js/hpa_apiversion.mdc +27 -0
- package/rules/k8s/js/ingress_gateway.mdc +16 -0
- package/rules/k8s/js/kustomize_structure.mdc +144 -0
- package/rules/k8s/js/lint_k8s.mdc +72 -0
- package/rules/k8s/js/multidoc_yaml.mdc +5 -0
- package/rules/k8s/js/network_policy.mdc +136 -0
- package/rules/k8s/js/schema_modeline.mdc +57 -0
- package/rules/k8s/js/service.mdc +44 -0
- package/rules/k8s/js/topology_hpa_pdb.mdc +181 -0
- package/rules/k8s/main.mdc +29 -834
- package/rules/k8s/policy/base_kustomization/base_kustomization.mdc +12 -0
- package/rules/k8s/policy/base_manifest/base_manifest.mdc +14 -0
- package/rules/k8s/policy/gateway/gateway.mdc +17 -0
- package/rules/k8s/policy/hasura_configmap/hasura_configmap.mdc +20 -0
- package/rules/k8s/policy/hasura_httproute/hasura_httproute.mdc +16 -0
- package/rules/k8s/policy/hpa_pdb/hpa_pdb.mdc +23 -0
- package/rules/k8s/policy/kustomization/kustomization.mdc +20 -0
- package/rules/k8s/policy/manifest/manifest.mdc +17 -0
- package/rules/k8s/policy/network_policy/network_policy.mdc +22 -0
- package/rules/k8s/policy/svc_hl_yaml/svc_hl_yaml.mdc +13 -0
- package/rules/k8s/policy/svc_yaml/svc_yaml.mdc +12 -0
- package/rules/nginx-default-tpl/js/dockerfile.mdc +36 -0
- package/rules/nginx-default-tpl/js/http-route.mdc +41 -0
- package/rules/nginx-default-tpl/js/ini-keys.mdc +21 -0
- package/rules/nginx-default-tpl/js/template-structure.mdc +86 -0
- package/rules/nginx-default-tpl/js/vscode.mdc +37 -0
- package/rules/nginx-default-tpl/main.mdc +8 -110
- package/rules/nginx-default-tpl/policy/vscode_extensions/vscode_extensions.mdc +11 -0
- package/rules/nginx-default-tpl/policy/vscode_settings/vscode_settings.mdc +15 -0
- package/rules/npm-module/js/docs/index.md +5 -5
- package/rules/npm-module/js/docs/rule_meta.md +6 -6
- package/rules/npm-module/js/docs/skill_meta.md +8 -8
- package/rules/npm-module/js/header_doc_pointer.mdc +18 -0
- package/rules/npm-module/js/package_structure.mdc +62 -0
- package/rules/npm-module/js/rule_meta.mdc +11 -0
- package/rules/npm-module/js/skill_meta.mdc +11 -0
- package/rules/npm-module/main.mdc +10 -52
- package/rules/npm-module/policy/emit_types_config/emit_types_config.mdc +40 -0
- package/rules/npm-module/policy/npm_package_json/npm_package_json.mdc +50 -0
- package/rules/npm-module/policy/root_package_json/root_package_json.mdc +37 -0
- package/rules/php/js/lint_php_yml.mdc +12 -0
- package/rules/php/js/tooling.mdc +66 -0
- package/rules/php/main.mdc +5 -66
- package/rules/php/policy/lint_php_yml/lint_php_yml.mdc +21 -0
- package/rules/python/js/lint_python_yml.mdc +23 -0
- package/rules/python/js/pyproject_toml.mdc +32 -0
- package/rules/python/js/tooling.mdc +23 -0
- package/rules/python/main.mdc +7 -32
- package/rules/python/policy/lint_python_yml/lint_python_yml.mdc +12 -0
- package/rules/python/policy/pyproject_toml/pyproject_toml.mdc +13 -0
- package/rules/rego/js/rego-lint.mdc +31 -0
- package/rules/rego/js/vscode_extensions.mdc +11 -0
- package/rules/rego/js/vscode_settings.mdc +13 -0
- package/rules/rego/main.mdc +10 -22
- package/rules/rego/policy/vscode_extensions/vscode_extensions.mdc +11 -0
- package/rules/rego/policy/vscode_settings/vscode_settings.mdc +19 -0
- package/rules/rust/js/coverage.mdc +28 -0
- package/rules/rust/js/lint.mdc +22 -0
- package/rules/rust/js/tauri_composition.mdc +8 -0
- package/rules/rust/js/vscode_extensions.mdc +12 -0
- package/rules/rust/main.mdc +8 -38
- package/rules/rust/policy/lint_rust_yml/lint_rust_yml.mdc +12 -0
- package/rules/rust/policy/vscode_extensions/vscode_extensions.mdc +9 -0
- package/rules/security/js/rego_policies.mdc +15 -0
- package/rules/security/js/sample_secret.mdc +19 -0
- package/rules/security/js/trufflehog.mdc +21 -0
- package/rules/security/main.mdc +7 -34
- package/rules/security/policy/lint_security_yml/lint_security_yml.mdc +7 -0
- package/rules/security/policy/package_json/package_json.mdc +7 -0
- package/rules/style/js/admin-table.mdc +88 -0
- package/rules/style/js/colors.mdc +21 -0
- package/rules/style/js/gap.mdc +22 -0
- package/rules/style/js/quasar-fixes.mdc +32 -0
- package/rules/style/js/quasar.mdc +7 -0
- package/rules/style/js/tooling.mdc +85 -0
- package/rules/style/main.mdc +12 -251
- package/rules/style/policy/lint_style_yml/lint_style_yml.mdc +13 -0
- package/rules/style/policy/package_json/package_json.mdc +18 -0
- package/rules/style/policy/vscode_extensions/vscode_extensions.mdc +13 -0
- package/rules/style/policy/vscode_settings/vscode_settings.mdc +19 -0
- package/rules/tauri/js/cargo_mutants_config.mdc +39 -0
- package/rules/tauri/js/tool_surface.mdc +21 -0
- package/rules/tauri/js/tooling.mdc +25 -0
- package/rules/tauri/main.mdc +6 -78
- package/rules/tauri/policy/vscode_extensions/vscode_extensions.mdc +21 -0
- package/rules/test/js/cargo_mutants_config.mdc +18 -0
- package/rules/test/js/docs/index.md +7 -7
- package/rules/test/js/location.mdc +52 -0
- package/rules/test/js/no-console-store-restore.mdc +11 -0
- package/rules/test/js/no-process-chdir.mdc +15 -0
- package/rules/test/js/no-relative-fs-path.mdc +22 -0
- package/rules/test/js/sandbox-aware-test.mdc +28 -0
- package/rules/test/js/stryker_config.mdc +26 -0
- package/rules/test/js/vitest-config-pool-forks.mdc +33 -0
- package/rules/test/main.mdc +16 -184
- package/rules/test/policy/package_json/package_json.mdc +16 -0
- package/rules/text/js/ci-lint-text.mdc +15 -0
- package/rules/text/js/cspell.mdc +81 -0
- package/rules/text/js/dotenv-linter.mdc +16 -0
- package/rules/text/js/forbidden-prettier.mdc +13 -0
- package/rules/text/js/markdownlint.mdc +25 -0
- package/rules/text/js/oxfmt.mdc +35 -0
- package/rules/text/js/package-json.mdc +26 -0
- package/rules/text/js/shellcheck.mdc +18 -0
- package/rules/text/js/v8r.mdc +23 -0
- package/rules/text/js/vscode.mdc +86 -0
- package/rules/text/main.mdc +20 -231
- package/rules/text/policy/cspell/cspell.mdc +34 -0
- package/rules/text/policy/lint_text/lint_text.mdc +19 -0
- package/rules/text/policy/markdownlint/markdownlint.mdc +38 -0
- package/rules/text/policy/oxfmtrc/oxfmtrc.mdc +11 -0
- package/rules/text/policy/package_json/package_json.mdc +33 -0
- package/rules/text/policy/vscode_extensions/vscode_extensions.mdc +13 -0
- package/rules/text/policy/vscode_settings/vscode_settings.mdc +13 -0
- package/rules/vue/js/composition-api.mdc +82 -0
- package/rules/vue/js/nheader-layout.mdc +171 -0
- package/rules/vue/js/node-imports.mdc +25 -0
- package/rules/vue/js/quasar-ui.mdc +32 -0
- package/rules/vue/js/structure.mdc +101 -0
- package/rules/vue/js/testing.mdc +32 -0
- package/rules/vue/js/tfm-translations.mdc +26 -0
- package/rules/vue/js/vite-config.mdc +126 -0
- package/rules/vue/js/vite-env.mdc +55 -0
- package/rules/vue/js/vue-imports.mdc +25 -0
- package/rules/vue/main.mdc +15 -641
- package/rules/vue/policy/package_json/package_json.mdc +30 -0
- package/scripts/docs/index.md +16 -16
- package/scripts/lib/docs/index.md +36 -36
- package/scripts/utils/docs/index.md +14 -14
package/rules/js-bun-db/main.mdc
CHANGED
|
@@ -5,621 +5,26 @@ 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
|
-
|
|
10
|
+
[js-bun-db-bun-sql-migration](./js/bun-sql-migration.mdc)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
[js-bun-db-pg-format-shim](./js/pg-format-shim.mdc)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
[js-bun-db-pg-listen-notify](./js/pg-listen-notify.mdc)
|
|
15
15
|
|
|
16
|
-
-
|
|
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
|
-
|
|
18
|
+
[js-bun-db-connection](./js/connection.mdc)
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
[js-bun-db-query-safety](./js/query-safety.mdc)
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
[js-bun-db-sql-array](./js/sql-array.mdc)
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
[js-bun-db-unsafe](./js/unsafe.mdc)
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
[js-bun-db-pg-leftover](./js/pg-leftover.mdc)
|
|
29
27
|
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
### Типові ідіоми `pg-format` → Bun SQL
|
|
37
|
-
|
|
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: повне видалення, без шимів`).
|
|
30
|
+
[js-bun-db-package-json](./policy/package_json/package_json.mdc)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## Заборонені залежності у package.json
|
|
2
|
+
|
|
3
|
+
Rego-пакет: `js_bun_db.package_json`
|
|
4
|
+
|
|
5
|
+
Цільові файли: `**/package.json`
|
|
6
|
+
|
|
7
|
+
Перевіряє поле `dependencies` на наявність пакетів із deny-списку [`package.json.deny.json`](./template/package.json.deny.json). Список керується даними (`--data`), тому розширюється без зміни rego-коду.
|
|
8
|
+
|
|
9
|
+
Поточний deny-список:
|
|
10
|
+
|
|
11
|
+
- `pg-format` — заміни на Bun native SQL (без ручного форматування)
|
|
12
|
+
- `mysql2` — заміни на Bun native SQL
|
|
13
|
+
|
|
14
|
+
`pg` у dependencies **не** є порушенням на рівні Rego — виняток для LISTEN/NOTIFY зважується у JS-сканері (`js/safety.mjs`).
|
|
15
|
+
|
|
16
|
+
✓ `{ "dependencies": { "pg": "^8.0.0" } }` — дозволено
|
|
17
|
+
✗ `{ "dependencies": { "pg-format": "^1.0.0" } }` — `deny: dependencies.pg-format — заміни на Bun native SQL`
|