@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/main.mdc
CHANGED
|
@@ -5,232 +5,40 @@ alwaysApply: false
|
|
|
5
5
|
version: '1.30'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
**oxlint**, **ESLint**, **jscpd**, **knip**. Запуск — **`n-cursor lint js`** (локально; у CI — `--read-only`, без **`--fix`** для oxlint/eslint
|
|
8
|
+
**oxlint**, **ESLint**, **jscpd**, **knip**. Запуск — **`n-cursor lint js`** (локально; у CI — `--read-only`, без **`--fix`** для oxlint/eslint). Без **prettier** і **@nitra/prettier-config**.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
[js-file-extensions](./js/file-extensions.mdc)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
"type": "module",
|
|
15
|
-
"devDependencies": {
|
|
16
|
-
"@nitra/eslint-config": "^3.10.0"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
```
|
|
12
|
+
[js-package-json](./js/package-json.mdc)
|
|
20
13
|
|
|
21
|
-
|
|
14
|
+
[js-eslint-config](./js/eslint-config.mdc)
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
[js-oxlintrc](./js/oxlintrc.mdc)
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
[js-extensions](./js/extensions.mdc)
|
|
26
19
|
|
|
27
|
-
-
|
|
28
|
-
- **`.cjs`** — для CommonJS, де він справді потрібен.
|
|
20
|
+
[js-jscpd](./js/jscpd.mdc)
|
|
29
21
|
|
|
30
|
-
|
|
22
|
+
[js-knip](./js/knip.mdc)
|
|
31
23
|
|
|
32
|
-
|
|
24
|
+
[js-dep-policy](./js/dep-policy.mdc)
|
|
33
25
|
|
|
34
|
-
|
|
26
|
+
[js-lint-js-workflow](./js/lint-js-workflow.mdc)
|
|
35
27
|
|
|
36
|
-
|
|
28
|
+
[js-utils-lib-structure](./js/utils-lib-structure.mdc)
|
|
37
29
|
|
|
38
|
-
|
|
30
|
+
[js-for-in](./js/for-in.mdc)
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
[js-tests](./js/tests.mdc)
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
"jsPlugins": ["@e18e/eslint-plugin"],
|
|
45
|
-
"rules": {
|
|
46
|
-
"e18e/prefer-includes": "error"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
34
|
+
## Швидкий gate через conftest
|
|
50
35
|
|
|
51
|
-
|
|
36
|
+
Rego-пакети у `policy/` — запускаються `npx @nitra/cursor fix js` або `conftest`:
|
|
52
37
|
|
|
53
|
-
|
|
38
|
+
[js-policy-package_json](./policy/package_json/package_json.mdc)
|
|
54
39
|
|
|
55
|
-
|
|
40
|
+
[js-policy-jscpd](./policy/jscpd/jscpd.mdc)
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
"gitignore": true,
|
|
60
|
-
"exitCode": 1,
|
|
61
|
-
"reporters": ["console"],
|
|
62
|
-
"minLines": 25,
|
|
63
|
-
"ignore": [".claude/worktrees/**", "**/dist/**", "**/CHANGELOG.md"]
|
|
64
|
-
}
|
|
65
|
-
```
|
|
42
|
+
[js-policy-lint_js_yml](./policy/lint_js_yml/lint_js_yml.mdc)
|
|
66
43
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```text title=".gitignore (фрагмент)"
|
|
70
|
-
.claude/worktrees/
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Залежнісна політика (що не додавати)
|
|
74
|
-
|
|
75
|
-
`@e18e/eslint-plugin` окремо не додавай — він уже в залежностях `@nitra/eslint-config` (з **3.8.0**), oxlint підвантажує його з `node_modules`. Пакети oxlint/eslint/jscpd/knip теж не додавай у `devDependencies` без потреби монорепо — `bunx` тягне їх ad-hoc.
|
|
76
|
-
|
|
77
|
-
## knip
|
|
78
|
-
|
|
79
|
-
Залежнісний аналіз (knip — невикористані залежності/експорти, `knip.json` канон) крос-файловий; запускається автоматично у `lint --full` разом з jscpd.
|
|
80
|
-
|
|
81
|
-
У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/rules/js/js/tooling/knip-canonical.json`](./js/tooling/knip-canonical.json). Він покриває типові false-positives для наших правил: `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`), `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`, `ignore` для `**/__fixtures__/**`, `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`, `graphql`), і `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
|
|
82
|
-
|
|
83
|
-
Якщо `knip.json` відсутній — `npx @nitra/cursor fix js` копіює канон у корінь проєкту (side effect). Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
|
|
84
|
-
|
|
85
|
-
Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
|
|
86
|
-
|
|
87
|
-
## Заборона `@nitra/as-integrations-fastify`
|
|
88
|
-
|
|
89
|
-
Пакет **`@nitra/as-integrations-fastify`** заборонений у **`dependencies`**, **`peerDependencies`** та в import-specifier-ах. Це чистий републіш upstream, застряглий на peer `@apollo/server: "^4.0.0"`, тож на Apollo 5 `bun install` дає `warn: incorrect peer dependency`. Заміна — upstream **`@as-integrations/fastify`** (**`^3.1.0`**): peer `@apollo/server: "^4.0.0 || ^5.0.0"` + `fastify: "^5.3.0"`.
|
|
90
|
-
|
|
91
|
-
Міграція — лише specifier у трьох місцях: залежність у `package.json`, `import`, та `vi.mock(...)` / `await import(...)` у тестах. **Код не міняється**, експорти ті самі: `default` → `fastifyApollo`, named → `fastifyApolloDrainPlugin`.
|
|
92
|
-
|
|
93
|
-
```javascript title="❌ до"
|
|
94
|
-
import fastifyApollo, { fastifyApolloDrainPlugin } from '@nitra/as-integrations-fastify'
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
```javascript title="✅ після"
|
|
98
|
-
import fastifyApollo, { fastifyApolloDrainPlugin } from '@as-integrations/fastify'
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## jscpd: рефакторинг і структура
|
|
102
|
-
|
|
103
|
-
Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
|
|
104
|
-
|
|
105
|
-
- **Рефакторинг:** винеси спільні фрагменти в функції, модулі, утиліти, composables/hooks, спільні компоненти або базові типи — залежно від контексту.
|
|
106
|
-
- **Структура:** якщо одна й та сама логіка розмазана між файлами чи пакетами, запропонуй зміну структури (наприклад, спільний модуль, `shared/`, внутрішній пакет у monorepo), щоб була **одна канонічна реалізація** і повторні місця лише викликали її.
|
|
107
|
-
|
|
108
|
-
Так ти уникаєш **хибних обходів** перевірки: розширення `ignore` чи завищення `minLines` лише щоб прибрати звіт — не заміна рефакторингу для справжніх клонів. Якщо збіг **семантично випадковий** (генерований код, формальні шаблони без спільної логіки), після оцінки допустимо точковий `ignore` або зміна порогу — з коротким обґрунтуванням.
|
|
109
|
-
|
|
110
|
-
Але обов'язково перед рефакторингом перевір чи є тести на блоки які підлягають зміні, а саме Bun.test для js та playwright для vue. Якщо є, то перевір чи вони покривають блоки які підлягають зміні. Якщо не покривають або тестів немає — спочатку створи їх, перевір що вони покривають і відпрацьовують коректно, а потім роби рефакторинг і ще раз запускай тести, але якщо тести не відпрацьовують коректно після рефакторингу, то не роби рефакторинг.
|
|
111
|
-
|
|
112
|
-
## Структура спільних модулів: `utils/` vs `lib/`
|
|
113
|
-
|
|
114
|
-
Коли спільні методи виносяться з кількох JS-файлів в окремий каталог — назву каталога обирай за призначенням модулів усередині.
|
|
115
|
-
|
|
116
|
-
- **`utils/`** — низькорівневі, чисті, generic helpers без бізнес-логіки і без залежностей від домену проєкту. Те, що могло б жити в окремому npm-пакеті. Приклади: `formatDate()`, `chunk(array, size)`, `retry(fn, opts)`, `deepMerge()`, `parseDuration('5m')`, `sleep(ms)`.
|
|
117
|
-
|
|
118
|
-
- **`lib/`** — внутрішні модулі / підсистеми проєкту, які знають про домен. Більші за `utils/`, часто з власним state, конфігом або side effects. Приклади: `lib/gmail-client.js`, `lib/circuit-breaker.js`, `lib/skill-loader.js`, `lib/safety/dry-run.js`.
|
|
119
|
-
|
|
120
|
-
Швидкий тест: якщо файл завтра можна опублікувати окремим npm-пакетом без переписування — це `utils/`; якщо він тримає domain-state, читає конфіг проєкту або викликає зовнішні сервіси/файли — це `lib/`. Не плутай із чужими каталогами на кшталт `shared/` чи `common/`: канонічні назви — лише `utils/` і `lib/`.
|
|
121
|
-
|
|
122
|
-
Автоматично гарантується: `npx @nitra/cursor fix js` (концерн `utils_imports`) обходить кожен `utils/`-каталог у воркспейсах і падає, якщо знаходить relative-імпорт з `..` (вихід за межі каталогу) у будь-якому не-тестовому файлі. Це механічне втілення правила «utils не знає про домен»: тільки same-dir (`./X`), bare-пакети та `node:*` дозволені; cross-rule, конфіги проєкту чи sibling-utils — fail. Якщо потрібна domain-залежність — перенеси файл у `lib/`.
|
|
123
|
-
|
|
124
|
-
Додай workflow:
|
|
125
|
-
|
|
126
|
-
```yaml title=".github/workflows/lint-js.yml"
|
|
127
|
-
name: Lint JS
|
|
128
|
-
|
|
129
|
-
on:
|
|
130
|
-
push:
|
|
131
|
-
branches:
|
|
132
|
-
- dev
|
|
133
|
-
- main
|
|
134
|
-
paths:
|
|
135
|
-
- '**/*.js'
|
|
136
|
-
- '**/*.mjs'
|
|
137
|
-
- '**/*.cjs'
|
|
138
|
-
- '**/*.jsx'
|
|
139
|
-
- '**/*.ts'
|
|
140
|
-
- '**/*.tsx'
|
|
141
|
-
- '**/*.vue'
|
|
142
|
-
- '**/eslint.config.*'
|
|
143
|
-
|
|
144
|
-
pull_request:
|
|
145
|
-
branches:
|
|
146
|
-
- dev
|
|
147
|
-
- main
|
|
148
|
-
|
|
149
|
-
concurrency:
|
|
150
|
-
group: ${{ github.ref }}-${{ github.workflow }}
|
|
151
|
-
cancel-in-progress: true
|
|
152
|
-
|
|
153
|
-
jobs:
|
|
154
|
-
eslint:
|
|
155
|
-
runs-on: ubuntu-latest
|
|
156
|
-
permissions:
|
|
157
|
-
contents: read
|
|
158
|
-
steps:
|
|
159
|
-
- uses: actions/checkout@v6
|
|
160
|
-
with:
|
|
161
|
-
persist-credentials: false
|
|
162
|
-
|
|
163
|
-
- uses: ./.github/actions/setup-bun-deps
|
|
164
|
-
|
|
165
|
-
- name: Eslint
|
|
166
|
-
run: |
|
|
167
|
-
bunx oxlint
|
|
168
|
-
bunx eslint .
|
|
169
|
-
bunx jscpd .
|
|
170
|
-
bunx knip --no-config-hints
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
Канон workflow `.github/workflows/lint-js.yml`: [lint-js.yml.snippet.yml](./policy/lint_js_yml/template/lint-js.yml.snippet.yml)
|
|
174
|
-
|
|
175
|
-
Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
|
|
176
|
-
|
|
177
|
-
Один workflow на лінт JS; зайвий `lint.yml` з тими самими кроками — прибери.
|
|
178
|
-
|
|
179
|
-
```javascript title="eslint.config.js"
|
|
180
|
-
import { getConfig } from '@nitra/eslint-config'
|
|
181
|
-
|
|
182
|
-
export default [
|
|
183
|
-
{
|
|
184
|
-
ignores: ['**/auto-imports.d.ts']
|
|
185
|
-
},
|
|
186
|
-
...getConfig({
|
|
187
|
-
node: ['npm']
|
|
188
|
-
})
|
|
189
|
-
]
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
У монорепо пакети з Vite (frontend) вкажи в секції `vue`, решту — у секції `node` у виклику `getConfig`.
|
|
193
|
-
|
|
194
|
-
## Додаткові js правила
|
|
195
|
-
|
|
196
|
-
Завжди додавай до package.json що підтримується 24+ версія node і Bun 1.3+:
|
|
197
|
-
|
|
198
|
-
```json title="package.json"
|
|
199
|
-
"engines": {
|
|
200
|
-
"node": ">=24",
|
|
201
|
-
"bun": ">=1.3"
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
## `for...in` заборонено — рефакторити на `for...of`
|
|
206
|
-
|
|
207
|
-
Конструкція `for (const k in obj)` обходить успадковані ключі прототипу, тому майже завжди тягне `Object.hasOwn(obj, k)`-guard. Заборонена у `@nitra/eslint-config` через `no-restricted-syntax` для `ForInStatement` (з версії **3.8.0**). У каноні oxlint лишається `guard-for-in` як часткова страховка (oxlint не підтримує `no-restricted-syntax`). Замість цього обходь масив напряму, а обʼєкт — через `Object.entries` / `Object.keys` / `Object.values`. У такому коді guard за `Object.hasOwn` стає непотрібним і має зникнути разом із `for...in`.
|
|
208
|
-
|
|
209
|
-
```javascript title="❌ до"
|
|
210
|
-
for (const k in obj) {
|
|
211
|
-
if (!Object.hasOwn(obj, k)) continue
|
|
212
|
-
use(k, obj[k])
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
for (const i in arr) {
|
|
216
|
-
use(arr[i])
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
```javascript title="✅ після"
|
|
221
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
222
|
-
use(k, v)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
for (const item of arr) {
|
|
226
|
-
use(item)
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## Тести
|
|
231
|
-
|
|
232
|
-
Проєкт має бути покритий unit-тестами (**Bun test**). Код: синтаксис Node **24+**, **top level await** (узгоджено з `engines.node` у `package.json`).
|
|
233
|
-
|
|
234
|
-
## Покриття + мутаційне тестування JS
|
|
235
|
-
|
|
236
|
-
Покриття + мутаційне тестування JS постачаються через `n-cursor coverage` (правило `test.mdc`). Реалізація провайдера — у `npm/rules/js/coverage/coverage.mjs`: `bun test --coverage --coverage-reporter=lcov` + `bunx stryker run`. Stryker конфігурується в `stryker.config.mjs` у JS-корені (single-package або `workspaces[0]`).
|
|
44
|
+
[js-policy-vscode_extensions](./policy/vscode_extensions/vscode_extensions.mdc)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Rego-gate `.jscpd.json`
|
|
2
|
+
|
|
3
|
+
Rego-пакет: `js_lint.jscpd`
|
|
4
|
+
|
|
5
|
+
Цільовий файл: `.jscpd.json` у корені проєкту.
|
|
6
|
+
|
|
7
|
+
Що перевіряється:
|
|
8
|
+
|
|
9
|
+
- `gitignore` — точна відповідність (`true`)
|
|
10
|
+
- `exitCode` — точна відповідність (`1`)
|
|
11
|
+
- `reporters` — subset-of: масив має містити всі елементи з канону (`["console"]`)
|
|
12
|
+
- `minLines` — число, значення >= канонічного порогу (`25`); більше дозволено
|
|
13
|
+
|
|
14
|
+
Канон-snippet: [.jscpd.json.snippet.json](./template/.jscpd.json.snippet.json)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Rego-gate `.github/workflows/lint-js.yml`
|
|
2
|
+
|
|
3
|
+
Rego-пакет: `js_lint.lint_js_yml`
|
|
4
|
+
|
|
5
|
+
Цільовий файл: `.github/workflows/lint-js.yml`.
|
|
6
|
+
|
|
7
|
+
Що перевіряється:
|
|
8
|
+
|
|
9
|
+
- Наявність усіх `uses:`-кроків з канону (subset-of по `jobs.eslint.steps`)
|
|
10
|
+
- Наявність усіх рядків `run:` з канону (substring-match по всіх кроках)
|
|
11
|
+
- `actions/checkout@v6` має мати `with.persist-credentials: false` (inverse-check)
|
|
12
|
+
- Заборона `--fix` у CI: `bunx oxlint ... --fix` або `eslint --fix` у будь-якому `run:` → deny
|
|
13
|
+
|
|
14
|
+
Канон-snippet: [lint-js.yml.snippet.yml](./template/lint-js.yml.snippet.yml)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## Rego-gate `package.json`
|
|
2
|
+
|
|
3
|
+
Rego-пакет: `js_lint.package_json`
|
|
4
|
+
|
|
5
|
+
Цільовий файл: `package.json` (корінь і workspace-пакети).
|
|
6
|
+
|
|
7
|
+
Що перевіряється:
|
|
8
|
+
|
|
9
|
+
- `"type"` — точна відповідність `"module"` (з канон-snippet)
|
|
10
|
+
- `scripts.lint-js` — нормалізована точна відповідність (collapse whitespace)
|
|
11
|
+
- `engines.node` — semver-парсинг: major >= 24 (inverse, без template)
|
|
12
|
+
- `engines.bun` — semver-парсинг: >= 1.3 (inverse, без template)
|
|
13
|
+
- `devDependencies["@nitra/eslint-config"]` — наявність ключа + semver >= порогу зі snippet (`^3.10.0`); `workspace:*` завжди проходить
|
|
14
|
+
|
|
15
|
+
Канон-snippet: [package.json.snippet.json](./template/package.json.snippet.json)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Rego-gate `.vscode/extensions.json`
|
|
2
|
+
|
|
3
|
+
Rego-пакет: `js_lint.vscode_extensions`
|
|
4
|
+
|
|
5
|
+
Цільовий файл: `.vscode/extensions.json`.
|
|
6
|
+
|
|
7
|
+
Що перевіряється:
|
|
8
|
+
|
|
9
|
+
- `recommendations` — subset-of: масив має містити всі три канонічні розширення (`dbaeumer.vscode-eslint`, `github.vscode-github-actions`, `oxc.oxc-vscode`)
|
|
10
|
+
|
|
11
|
+
Канон-snippet: [extensions.json.snippet.json](./template/extensions.json.snippet.json)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## Перехід з `pg` / `mysql2` на Bun native SQL
|
|
2
|
+
|
|
3
|
+
PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом, підключаємось як `mysql://`).
|
|
4
|
+
|
|
5
|
+
Якщо в проєкті використовуються бібліотеки `pg`, `pg-format` або `mysql2`, їх потрібно замінити на Bun native SQL: <https://bun.com/docs/runtime/sql>.
|
|
6
|
+
|
|
7
|
+
- Видалити з `dependencies`: `pg-pool`, `pg-native`, `pg-format`, `mysql`, `mysql2`.
|
|
8
|
+
- Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
|
|
9
|
+
- Замінити на `import { sql, SQL } from 'bun'` — Bun має вбудований клієнт із пулом, prepared statements та tagged templates.
|
|
10
|
+
|
|
11
|
+
Канон заборонених `dependencies` (`pg-format`, `mysql2`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json). Сам `pg` із денилисту прибрано — він має одне легітимне виключення (LISTEN/NOTIFY), яке зважує AST-сканер; деталі — у `js/pg-listen-notify.mdc`.
|
|
12
|
+
|
|
13
|
+
`pg-format` (unscoped) — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (`sql\`... ${value} ...\``) і не лишає простору для injection — окремий «форматер» **для значень** не потрібен.
|
|
14
|
+
|
|
15
|
+
Виключення є **лише** для **динамічних identifiers** (назви схем / таблиць / колонок / індексів / ролей / БД) і whitelist-фрагментів типу `ASC`/`DESC`: Bun SQL їх параметризувати не вміє, тож тут дозволено окремий пакет **`@scaleleap/pg-format`** (scoped форк, не unscoped `pg-format`) — деталі й приклади у `js/pg-format-identifiers.mdc`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
## Підключення (singleton + env)
|
|
2
|
+
|
|
3
|
+
Дефолтний експорт `sql` з `'bun'` сам читає змінні середовища (`DATABASE_URL`, `POSTGRES_URL`, `MYSQL_URL`, `PGHOST`/`PGUSER`/... та `MYSQL_HOST`/`MYSQL_USER`/...) і керує пулом — окремий `Pool` як у `pg` створювати не треба.
|
|
4
|
+
|
|
5
|
+
Для явного конфігу — `new SQL(...)` як **singleton** на рівні модуля, а не на кожен запит. Файл кладеться у `src/conn/db.mjs` і експортує іменовані константи `pgWrite` (основний запис) та `pgRead` (read-only replica), щоб glob `**/src/conn/**` у правилах покривав ці файли:
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
// src/conn/db.mjs
|
|
9
|
+
import { SQL } from 'bun'
|
|
10
|
+
|
|
11
|
+
export const pgWrite = new SQL({
|
|
12
|
+
url: process.env.DATABASE_URL,
|
|
13
|
+
max: 20,
|
|
14
|
+
idleTimeout: 30,
|
|
15
|
+
connectionTimeout: 10
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const pgRead = new SQL({
|
|
19
|
+
url: process.env.PG_CONN_READ ?? process.env.DATABASE_URL,
|
|
20
|
+
max: 10,
|
|
21
|
+
idleTimeout: 30,
|
|
22
|
+
connectionTimeout: 10
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Connection string обирає адаптер автоматично:
|
|
27
|
+
|
|
28
|
+
- `postgres://...` / `postgresql://...` → PostgreSQL
|
|
29
|
+
- `mysql://...` / `mysql2://...` → MySQL/MariaDB
|
|
30
|
+
- `sqlite://...` / `file://...` / `:memory:` → SQLite
|
|
31
|
+
|
|
32
|
+
### Не створювати підключення на кожен запит
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
// ❌ нове підключення/інстанс на кожен виклик
|
|
36
|
+
function getUser(id) {
|
|
37
|
+
const pgWrite = new SQL(process.env.DATABASE_URL)
|
|
38
|
+
return pgWrite`SELECT * FROM users WHERE id = ${id}`
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`new SQL(...)` має створюватись **один раз** на рівні модуля. Bun сам тримає пул (`max`, `idleTimeout`, `maxLifetime`) — окремих `Pool`/`Client` як у `pg` не потрібно.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
## Динамічна SQL-структура: `@scaleleap/pg-format` для identifiers
|
|
2
|
+
|
|
3
|
+
Bun SQL **не вміє** параметризувати назви схем, таблиць, колонок, індексів, ролей, БД — а `sql\`SELECT * FROM ${table}\`` забіндив би це як значення і зламав би синтаксис. Для **динамічних identifiers** дозволено окремий пакет:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun add @scaleleap/pg-format
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
⚠️ Це **scoped `@scaleleap/pg-format`**, а не unscoped `pg-format` (той у [deny-списку](./policy/package_json/template/package.json.deny.json)). Беремо форк `@scaleleap` **тільки** заради `%I` / `%s`-можливостей; значення все одно проходять через Bun parameters, **не** через `%L`.
|
|
10
|
+
|
|
11
|
+
### Дозволений патерн
|
|
12
|
+
|
|
13
|
+
- **`%I`** — escape SQL identifier (schema / table / column / index / role / database).
|
|
14
|
+
- **`%s`** — raw fragment, **тільки** для whitelist-значень (`ASC` / `DESC`, тип JOIN'у тощо).
|
|
15
|
+
- Значення — позиційні параметри `$1, $2, …`, які передаються другим аргументом у `sql.unsafe(query, [bindParams])`.
|
|
16
|
+
- На рядку виклику `sql.unsafe(...)` обов'язковий маркер `// allow-unsafe: <причина>` (div. `js/unsafe.mdc`).
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
import format from '@scaleleap/pg-format'
|
|
20
|
+
import { sql } from 'bun'
|
|
21
|
+
|
|
22
|
+
const allowedColumns = new Set(['created_at', 'email', 'name'])
|
|
23
|
+
if (!allowedColumns.has(sortBy)) throw new Error('Invalid sort column')
|
|
24
|
+
|
|
25
|
+
const direction = sortDir === 'asc' ? 'ASC' : 'DESC'
|
|
26
|
+
|
|
27
|
+
const query = format(
|
|
28
|
+
'SELECT * FROM %I.%I ORDER BY %I %s LIMIT $1',
|
|
29
|
+
schemaName,
|
|
30
|
+
tableName,
|
|
31
|
+
sortBy,
|
|
32
|
+
direction
|
|
33
|
+
)
|
|
34
|
+
// allow-unsafe: динамічні schema/table/column; значення біндяться через $N
|
|
35
|
+
const rows = await sql.unsafe(query, [limit])
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Multi-row `INSERT` через `VALUES %L` теж типовий легітимний кейс, але передавай значення колонок як паралельні масиви через `unnest(...)` Bun SQL — `format('VALUES %L', rows)` лишай тільки коли альтернатива з `unnest` неможлива:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
const query = format(
|
|
42
|
+
/* sql */ `
|
|
43
|
+
INSERT INTO "order".delivery_status (order_id, status, changed_at)
|
|
44
|
+
SELECT v.order_id::uuid, v.status, v.changed_at::timestamptz
|
|
45
|
+
FROM (VALUES %L) AS v(order_id, status, changed_at)
|
|
46
|
+
`,
|
|
47
|
+
values
|
|
48
|
+
)
|
|
49
|
+
// allow-unsafe: multi-row VALUES для бекфілу; values формуються з валідованого input
|
|
50
|
+
await sql.unsafe(query)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Заборонено й після підключення `@scaleleap/pg-format`
|
|
54
|
+
|
|
55
|
+
- **`%L` для user input** — це повернення `pg-format`-стилю. Завжди bind через Bun (`sql\`... = ${value}\``) або позиційний параметр `$N` + `sql.unsafe(query, [params])`.
|
|
56
|
+
- Збирати весь `WHERE` через `format(...)` з `%L` — користуйся whitelist полів і ручним складанням `$N`-placeholder'ів (приклад нижче).
|
|
57
|
+
- Власні функції `format` / `pgFormat` / `sqlFormat` / `pgFmt` з тілом, що містить `%L` / `%I` / `%s`, — `fail` сканера (це шим, а не імпорт з бібліотеки).
|
|
58
|
+
- Експортовані `quoteLiteral` / `quoteIdent` / `escapeLiteral` / `escapeIdent` — `fail` сканера (pg-format-специфічні API замість Bun parameters).
|
|
59
|
+
|
|
60
|
+
### Dynamic `WHERE` — без `format(...)`, через whitelist + `$N`
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
const conditions = []
|
|
64
|
+
const values = []
|
|
65
|
+
|
|
66
|
+
if (email) {
|
|
67
|
+
values.push(email)
|
|
68
|
+
conditions.push(`email = $${values.length}`)
|
|
69
|
+
}
|
|
70
|
+
if (status) {
|
|
71
|
+
values.push(status)
|
|
72
|
+
conditions.push(`status = $${values.length}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
76
|
+
const query = `SELECT * FROM users ${where}`
|
|
77
|
+
// allow-unsafe: динамічний WHERE з whitelist-полів; значення біндяться через $N
|
|
78
|
+
const rows = await sql.unsafe(query, values)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Коротка таблиця рішень
|
|
82
|
+
|
|
83
|
+
| Сценарій | Що використовувати |
|
|
84
|
+
| --------------------------------- | ---------------------------------------------------- |
|
|
85
|
+
| `WHERE id = ${...}` | Bun SQL tagged template |
|
|
86
|
+
| `INSERT` одного рядка | Bun SQL tagged template |
|
|
87
|
+
| `INSERT` масиву (object/colset) | Bun SQL helper `sql(rows, 'a', 'b')` або `unnest` |
|
|
88
|
+
| `UPDATE field = ${value}` | Bun SQL tagged template |
|
|
89
|
+
| Динамічна назва schema / table | `@scaleleap/pg-format` `%I` + `sql.unsafe(q, [...])` |
|
|
90
|
+
| Динамічна назва колонки | `@scaleleap/pg-format` `%I` + bind |
|
|
91
|
+
| Динамічний `ORDER BY column` | whitelist + `%I` |
|
|
92
|
+
| `ASC` / `DESC`, тип JOIN'у | whitelist + `%s` |
|
|
93
|
+
| Динамічний `WHERE` (полів багато) | whitelist + ручні `$N` + `sql.unsafe(text, vals)` |
|
|
94
|
+
| Сирий migration / DDL | `sql.unsafe(text)` з `// allow-unsafe: <причина>` |
|
|
95
|
+
| User input як value | **тільки** Bun parameters / `$N` bind |
|
|
96
|
+
| Масив значень у `unnest(...)` | `sql.array(arr, type)` — обов'язково з типом |
|
|
97
|
+
|
|
98
|
+
Головне правило:
|
|
99
|
+
|
|
100
|
+
- **SQL values** → Bun SQL parameters (tagged template `${value}` або `$N` + `sql.unsafe(text, values)`).
|
|
101
|
+
- **SQL identifiers** → `@scaleleap/pg-format` `%I` (schema, table, column, index, role, database).
|
|
102
|
+
- **SQL fragments** (`ASC`/`DESC` тощо) → whitelist + `%s`.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
## `pg-format`: повне видалення, без шимів
|
|
2
|
+
|
|
3
|
+
Міграція з `pg-format` — це **зміна стилю запитів**, а не збереження API. У проєкті після переходу на Bun SQL **заборонено** залишати:
|
|
4
|
+
|
|
5
|
+
- функцію з іменем `format` (чи `pgFormat`, `sqlFormat`, `pgFmt`), що приймає шаблон з `%L` / `%I` / `%s` і значення;
|
|
6
|
+
- допоміжні `quoteLiteral`, `quoteIdent`, `escapeLiteral`, `escapeIdent` як публічні експорти модуля;
|
|
7
|
+
- обгортки `pgRead.query(text, params)` / `pgWrite.query(text, params)` / `db.query(text, params)`, які складають SQL-рядок (з або без `format`) і викликають `sql.unsafe(text, params)` — це повертає injection-поверхню, від якої ми йдемо, тільки під «зручним» іменем.
|
|
8
|
+
|
|
9
|
+
Замість цього всі точки використання потрібно перевести на tagged template `sql\`...\${value}...\``. Кожне `${value}` стає окремим параметром bind, без рядкового екранування.
|
|
10
|
+
|
|
11
|
+
### Типові ідіоми `pg-format` → Bun SQL
|
|
12
|
+
|
|
13
|
+
| Було (`pg-format`) | Стало (Bun SQL) |
|
|
14
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
15
|
+
| `format('... WHERE id = %L', id)` | `sql\`... WHERE id = ${id}\`` |
|
|
16
|
+
| `format('... IN (%L)', ids)` | `sql\`... IN ${sql(ids)}\`` (з guard на пустоту перед запитом) |
|
|
17
|
+
| `format('INSERT ... VALUES %L', [row])` (1 рядок) | `sql\`INSERT ... VALUES (${a}, ${b}, ...)\`` |
|
|
18
|
+
| `format('INSERT ... VALUES %L', rows)` (N рядків) | `sql\`INSERT INTO t ${sql(rows, 'a', 'b')}\`` або `unnest($1::T[], $2::T[]) AS t(a, b)` |
|
|
19
|
+
| `format('MERGE ... USING (VALUES %L) AS d(...)')` | `sql\`MERGE ... USING (SELECT * FROM unnest(${arrA}::A[], ${arrB}::B[]) AS t(a, b)) AS d\`` |
|
|
20
|
+
| `format('... %I ...', tableName)` (whitelist) | `@scaleleap/pg-format`: `format('%I', name)` + `sql.unsafe(text, [params])` з маркером |
|
|
21
|
+
|
|
22
|
+
Для multi-row `VALUES` у `MERGE` / `INSERT` з конкретними типами — паралельні масиви по колонках і `unnest($1::TYPE[], $2::TYPE[], ...) AS t(col1, col2, ...)`. Кожна колонка передається одним параметром-масивом; типи задаються кастом масиву (`::uuid[]`, `::bigint[]`, `::numeric[]`, `::text[]`, …).
|
|
23
|
+
|
|
24
|
+
#### Приклад: MERGE з UNNEST і динамічними колонками
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
// ❌ format + pgWrite.unsafe — N×7 окремих значень, план змінюється при кожному batch.length
|
|
28
|
+
const valuesSql = batch
|
|
29
|
+
.map(row => format('(%L::int, %L::date, %L::jsonb)', row.id, row.date, JSON.stringify(row.data)))
|
|
30
|
+
.join(',')
|
|
31
|
+
const sql = format(`MERGE INTO t USING (VALUES %s) AS s(id, date, data) ON ...`, valuesSql)
|
|
32
|
+
await pgWrite.unsafe(sql)
|
|
33
|
+
|
|
34
|
+
// ✅ UNNEST — 3 параметри незалежно від розміру batch; план стабільний і може кешуватись
|
|
35
|
+
await pgWrite`
|
|
36
|
+
WITH s(id, date, data) AS (
|
|
37
|
+
SELECT * FROM unnest(
|
|
38
|
+
${pgWrite.array(col(batch, 'id'), 'int4')},
|
|
39
|
+
${pgWrite.array(col(batch, 'date'), 'date')},
|
|
40
|
+
${pgWrite.array(col(batch, 'data'), 'jsonb')}
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
MERGE INTO my_table AS t
|
|
44
|
+
USING s ON t.id = s.id
|
|
45
|
+
WHEN MATCHED THEN
|
|
46
|
+
UPDATE SET date = s.date, data = s.data
|
|
47
|
+
WHEN NOT MATCHED THEN
|
|
48
|
+
INSERT (id, date, data) VALUES (s.id, s.date, s.data)
|
|
49
|
+
`
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Якщо частина колонок у SET/INSERT залежить від параметра (plan/fact, тип тощо) — динамічні імена колонок не можна параметризувати через `${value}`; використовуй умовні Bun SQL фрагменти:
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
// ✅ умовні фрагменти для динамічних ідентифікаторів колонок
|
|
56
|
+
const colFrag = isPlan ? pgWrite`plan_value` : pgWrite`fact_value`
|
|
57
|
+
const hashFrag = isPlan ? pgWrite`hash = s.hash,` : pgWrite``
|
|
58
|
+
|
|
59
|
+
await pgWrite`
|
|
60
|
+
...
|
|
61
|
+
WHEN MATCHED THEN
|
|
62
|
+
UPDATE SET
|
|
63
|
+
${colFrag} = s.value,
|
|
64
|
+
${hashFrag}
|
|
65
|
+
updated_by = s.updated_by
|
|
66
|
+
WHEN NOT MATCHED THEN
|
|
67
|
+
INSERT (id, ${colFrag}, updated_by)
|
|
68
|
+
VALUES (s.id, s.value, s.updated_by)
|
|
69
|
+
`
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Заборонений «drop-in» шим
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
// ❌ pg-format-сумісний шим, що ховає `unsafe` під «безпечним» іменем
|
|
76
|
+
export function format(fmt, ...args) {
|
|
77
|
+
let i = 0
|
|
78
|
+
return fmt.replaceAll(/%[LIs]/g, () => quoteLiteral(args[i++]))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ❌ і його типовий call-site — той самий injection-вектор, що і прямий sql.unsafe із конкатенацією
|
|
82
|
+
await sql.unsafe(format('... WHERE id = %L', userId))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
// ❌ pg-сумісна обгортка над Bun SQL — ще один прихований `unsafe`
|
|
87
|
+
export const pgWrite = {
|
|
88
|
+
query(text, params) {
|
|
89
|
+
return sql.unsafe(text, params)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
// ✅ напряму tagged template — параметризація через wire-protocol bind
|
|
96
|
+
await sql`... WHERE id = ${userId}`
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Виняток для шиму `format()` під час поетапної міграції допускається **тільки** в окремому commit'і з TODO-маркером і дедлайном; готовий код у main з таким шимом — `fail` правила (детектори: `pg-format shim`, `query wrapper over unsafe`).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## Прибирати pg-leftover виклики (`.connect()`, `.end()`)
|
|
2
|
+
|
|
3
|
+
У файлах з Bun SQL (`import { sql, SQL } from 'bun'`) залишки від `pg` — `pool.connect()`, `client.end()`, `pool.end()` — мають бути видалені. Bun SQL пулом керує сам: на першому запиті підключається, idle/lifetime закриває за конфігом — окремий життєвий цикл вручну не потрібен.
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
// ❌ pg-leftover: ручний lifecycle, який Bun SQL робить за тебе
|
|
7
|
+
const client = await pool.connect()
|
|
8
|
+
try {
|
|
9
|
+
await client.query('...')
|
|
10
|
+
} finally {
|
|
11
|
+
await client.end()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ✅ Bun SQL — без явних .connect()/.end()
|
|
15
|
+
await sql`...`
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Якщо виклик дійсно потрібен (наприклад, `sql.end()` у graceful shutdown або `.connect()` на сторонньому об'єкті, що випадково ділить імʼя методу), додай маркер `// allow-pg-leftover: <причина>` на тому ж рядку (trailing) або на рядку безпосередньо перед викликом:
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
// allow-pg-leftover: graceful shutdown — закриваємо пул перед exit
|
|
22
|
+
await sql.end()
|
|
23
|
+
|
|
24
|
+
ws.connect(url) // allow-pg-leftover: WebSocket, не pg
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Формат маркера: `allow-pg-leftover: <непорожня причина>` у line- або block-коментарі. Без маркера й без причини — **fail** перевірки.
|